Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Stuart Bishop | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merge reported by: | Martin Pool | ||||||||
Merged at revision: | not available | ||||||||
Proposed branch: | lp:~mbp/launchpad/dkim | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
491 lines (+359/-8) 5 files modified
lib/canonical/launchpad/mail/incoming.py (+89/-2) lib/lp/services/mail/tests/test_dkim.py (+258/-0) lib/lp/services/mail/tests/test_signedmessage.py (+6/-6) setup.py (+3/-0) versions.cfg (+3/-0) |
||||||||
To merge this branch: | bzr merge lp:~mbp/launchpad/dkim | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Review via email: mp+26284@code.launchpad.net |
Commit message
Description of the change
progress towards treating DKIM mail as authenticated
see https:/
and https:/
Martin Pool (mbp) wrote : | # |
Stuart Bishop (stub) wrote : | # |
Thanks for this. I'll certainly appreciate this feature :-)
=== modified file 'lib/canonical/
+def _authenticateDk
+ """"Attempt DKIM authentication of email; return True if known authentic"""
+
+ try:
+ dkim_result = dkim.verify(
+ except dkim.DKIMException, e:
+ log.warning('DKIM error: %r' % (e,))
+ dkim_result = False
+ except dns.exception.
+ # many of them have lame messages, thus %r
+ log.warning('DNS exception: %r' % (e,))
+ dkim_result = False
So this is failing DKIM authentication on DNS glitches, and DKIMException (invalid signature, messed up DNS etc.). I think users need to be informed of this. A the moment, they will just get the standard 'unsigned email' response with no hint what went wrong.
This might be too much refactoring for now, so perhaps a bug is in order and we can worry about it if it is a genuine problem. I think we would have to annotate the 'mail' object and have a higher level report failures to the user if strong authentication is required but not available.
+ if mail.signature:
+ log.info('message has gpg signature, therefore not treating DKIM as conclusive')
+ return False
I think this logic is better left to the callsite. Just return success/fail and leave it up to authenticateEmail what signature types should take precidence.
=== added file 'lib/lp/
+# reuse the handler that records log events
+from lp.services.
You don't seem to be using this. If you do, ideally this should be moved somewhere like lp.testing.
+ def monkeypatch_
+ self._dns_responses = {}
+ def my_lookup(name):
+ try:
+ return self._dns_
+ except KeyError:
+ raise dns.resolver.
+ orig_dnstxt = dkim.dnstxt
+ dkim.dnstxt = my_lookup
+ def restore():
+ dkim.dnstxt = orig_dnstxt
+ self.addCleanup
This might have been nicer using Mocker (http://
+ def test_dkim_
+ # FIXME: this fails because some re-wrapping happens after the
+ # message comes in (probably going too/from SignedMessage)
+ # and pydkim correctly complains about that: we would need to
+ # validate against the raw bytes
+ return
+ signed_message = self.fake_
+ canonicalize=
+ self._dns_
+ sample_dns
+ principal = authenticateEma
+ self.assertStro
This test is still disabled. Does this need to be fixed before landing, as we would see this situation in real email too? If not, at a minimum the comment needs to change to an XXX referencing a Launchpad bug.
+ def test_d...
Martin Pool (mbp) wrote : | # |
> Thanks for this. I'll certainly appreciate this feature :-)
Thanks for the review.
>
> === modified file 'lib/canonical/
>
> +def _authenticateDk
> + """"Attempt DKIM authentication of email; return True if known
> authentic"""
> +
> + try:
> + dkim_result = dkim.verify(
> + except dkim.DKIMException, e:
> + log.warning('DKIM error: %r' % (e,))
> + dkim_result = False
> + except dns.exception.
> + # many of them have lame messages, thus %r
> + log.warning('DNS exception: %r' % (e,))
> + dkim_result = False
>
> So this is failing DKIM authentication on DNS glitches, and DKIMException
> (invalid signature, messed up DNS etc.). I think users need to be informed of
> this. A the moment, they will just get the standard 'unsigned email' response
> with no hint what went wrong.
>
> This might be too much refactoring for now, so perhaps a bug is in order and
> we can worry about it if it is a genuine problem. I think we would have to
> annotate the 'mail' object and have a higher level report failures to the user
> if strong authentication is required but not available.
I think for now we shouldn't send any mail on DKIM failures, because that would be a likely way for this to go wrong by spamming people. Let's get some experience on real data first by monitoring how many fail and why.
There are a few possibilities:
* message is gpg signed but has broken dkim
* message has a broken signature but it's ok for it to be low-trusted anyhow
* message left the user ok but was broken somewhere in transit
* message was broken in our DC on the way to the incoming queue (we don't know this won't happen)
* message failed to verify because of a transient dns error
If the user expects their messages to be trusted and they aren't, there should be a way for them to find out why. Conversely if they don't care about DKIM (and often it won't be under the user's control to fix) we shouldn't bother them about it. For the moment I think we should let them ask a question in the first case.
>
>
> + if mail.signature:
> + log.info('message has gpg signature, therefore not treating DKIM as
> conclusive')
> + return False
>
> I think this logic is better left to the callsite. Just return success/fail
> and leave it up to authenticateEmail what signature types should take
> precidence.
ok
>
> === added file 'lib/lp/
>
> +# reuse the handler that records log events
> +from lp.services.
>
> You don't seem to be using this. If you do, ideally this should be moved
> somewhere like lp.testing.
ok
>
>
> + def monkeypatch_
> + self._dns_responses = {}
> + def my_lookup(name):
> + try:
> + return self._dns_
> + except KeyError:
> + raise dns.resolver.
> + orig_dnstxt = dkim.dnstxt
> + dkim.dnstxt = my_lookup
> + def restore():
> + dkim.dnstxt = orig_dnstxt
> + self.addCleanup
>
> Thi...
Martin Pool (mbp) wrote : | # |
bug 587783 now has what I think is a good clean patch, and a script to demonstrate it, and I've just sent it upstream.
There is also a source package branch there that can build a .deb of the fixed version or I guess that can be turned into an egg if prefered.
There was some list discussion of this <https:/
The point was also raised that we might want a whitelist of domains who are trusted to have meaningful DKIM headers, so I'll add that and do stub's tweaks.
Martin Pool (mbp) wrote : | # |
>
> >
> >
> > + if mail.signature:
> > + log.info('message has gpg signature, therefore not treating DKIM as
> > conclusive')
> > + return False
> >
> > I think this logic is better left to the callsite. Just return success/fail
> > and leave it up to authenticateEmail what signature types should take
> > precidence.
>
> ok
done
>
> >
> > === added file 'lib/lp/
> >
> > +# reuse the handler that records log events
> > +from lp.services.
> >
> > You don't seem to be using this. If you do, ideally this should be moved
> > somewhere like lp.testing.
>
> ok
fixed
> >
> > + def test_dkim_
> > + # FIXME: this fails because some re-wrapping happens after the
> > + # message comes in (probably going too/from SignedMessage)
> > + # and pydkim correctly complains about that: we would need to
> > + # validate against the raw bytes
> > + return
> > + signed_message = self.fake_
> > + canonicalize=
> > + self._dns_
> > + sample_dns
> > + principal =
> > authenticateEma
> > + self.assertStro
> >
> > This test is still disabled. Does this need to be fixed before landing, as
> we
> > would see this situation in real email too? If not, at a minimum the comment
> > needs to change to an XXX referencing a Launchpad bug.
>
> Yes, this needs to be fixed before landing; it's due in part to bug 587783.
fixed; there was a bug in python-dkim but I also need to expose the raw bytes of the message for DKIM validation, not the ISignedMessage which doesn't give the contents back byte-for-byte
>
> > + def test_dkim_
> > + # The message message has a syntactically valid DKIM signature that
> > + # doesn't actually correspond to the sender. We log something
> about
> > + # this but we don't want to drop the message.
> > + #
> > + # XXX: This test relies on having real DNS service to look up the
> > + # signing key.
> >
> > Is the XXX comment still valid now you are monkey patching the DNS
> responses?
> > We might have to fix this before landing - I don't know if this is an
> allowed
> > dependency in our tests.
>
> It's out of date.
fixed
I think the main things to do now are
* add a config option for a list of trusted domains and test it
* possibly get a final check-over from @elmo
* get the dependency included -- if you want, you could start that now in parallel?
* deploy to staging and test there?
Martin Pool (mbp) wrote : | # |
A couple more things:
* We need to check the From address matches the signature; at the moment we don't but I now have a failing test for it
* python-dkim only checks the first signature so a multiply signed message may be seen as untrusted. I think this is fine for now.
Stuart Bishop (stub) wrote : | # |
lp:~stub/launchpad/dkim contains the changes for building the patched pydkim with buildout
Martin Pool (mbp) wrote : | # |
<https:/
Stuart Bishop (stub) : | # |
Martin Pool (mbp) wrote : | # |
Sorry, could you please re-try with the import restored?
Martin Pool (mbp) wrote : | # |
the equivalent was merged by stub
Preview Diff
1 | === modified file 'lib/canonical/launchpad/mail/incoming.py' |
2 | --- lib/canonical/launchpad/mail/incoming.py 2010-06-04 16:15:38 +0000 |
3 | +++ lib/canonical/launchpad/mail/incoming.py 2010-07-12 20:52:02 +0000 |
4 | @@ -11,9 +11,13 @@ |
5 | from cStringIO import StringIO as cStringIO |
6 | from email.utils import getaddresses, parseaddr |
7 | import email.errors |
8 | +import logging |
9 | import re |
10 | import sys |
11 | |
12 | +import dkim |
13 | +import dns.exception |
14 | + |
15 | import transaction |
16 | from zope.component import getUtility |
17 | from zope.interface import directlyProvides, directlyProvidedBy |
18 | @@ -65,10 +69,83 @@ |
19 | """The account for the person sending this email is inactive.""" |
20 | |
21 | |
22 | -def authenticateEmail(mail): |
23 | +def extract_address_domain(address): |
24 | + realname, email_address = email.utils.parseaddr(address) |
25 | + return email_address.split('@')[1] |
26 | + |
27 | + |
28 | +_trusted_dkim_domains = [ |
29 | + 'gmail.com', 'google.com', 'mail.google.com', 'canonical.com'] |
30 | + |
31 | + |
32 | +def _isDkimDomainTrusted(domain): |
33 | + # XXX: really this should come from a dynamically-modifiable |
34 | + # configuration, but we don't have such a thing yet. |
35 | + # |
36 | + # Being listed here means that we trust the domain not to be an open relay |
37 | + # or to allow arbitrary intra-domain spoofing. |
38 | + return domain in _trusted_dkim_domains |
39 | + |
40 | + |
41 | +def _authenticateDkim(signed_message, raw_mail): |
42 | + """"Attempt DKIM authentication of email; return True if known authentic |
43 | + |
44 | + :param signed_message: ISignedMessage |
45 | + :param raw_mail: byte string of the raw contents: needed because |
46 | + DKIM may be in strict mode and we want to avoid any header rewrapping |
47 | + involved in parsing and regenerating the message. |
48 | + """ |
49 | + |
50 | + log = logging.getLogger('mail-authenticate-dkim') |
51 | + log.setLevel(logging.DEBUG) |
52 | + # uncomment this for easier test debugging |
53 | + # log.addHandler(logging.FileHandler('/tmp/dkim.log')) |
54 | + |
55 | + dkim_log = cStringIO() |
56 | + log.info('Attempting DKIM authentication of message %s from %s' |
57 | + % (signed_message['Message-ID'], signed_message['From'])) |
58 | + signing_details = [] |
59 | + try: |
60 | + # NB: if this fails with a keyword argument error, you need the |
61 | + # python-dkim 0.3-3.2 that adds it |
62 | + dkim_result = dkim.verify(raw_mail, dkim_log, details=signing_details) |
63 | + except dkim.DKIMException, e: |
64 | + log.warning('DKIM error: %r' % (e,)) |
65 | + dkim_result = False |
66 | + except dns.exception.DNSException, e: |
67 | + # many of them have lame messages, thus %r |
68 | + log.warning('DNS exception: %r' % (e,)) |
69 | + dkim_result = False |
70 | + else: |
71 | + log.info('DKIM verification result=%s' % (dkim_result,)) |
72 | + log.debug('DKIM debug log: %s' % (dkim_log.getvalue(),)) |
73 | + if not dkim_result: |
74 | + return False |
75 | + # in addition to the dkim signature being valid, we have to check that it |
76 | + # was actually signed by the user's domain. |
77 | + if len(signing_details) != 1: |
78 | + log.errors('expected exactly one DKIM details record: %r' % (signing_details,)) |
79 | + return False |
80 | + signing_domain = signing_details[0]['d'] |
81 | + from_domain = extract_address_domain(signed_message['From']) |
82 | + if signing_domain != from_domain: |
83 | + log.warning("DKIM signing domain %s doesn't match From address %s; " |
84 | + "disregarding signature" |
85 | + % (signing_domain, from_domain)) |
86 | + return False |
87 | + if not _isDkimDomainTrusted(signing_domain): |
88 | + log.warning("valid DKIM signature from untrusted domain %s" |
89 | + % (signing_domain,)) |
90 | + return False |
91 | + return True |
92 | + |
93 | + |
94 | +def authenticateEmail(mail, raw_mail): |
95 | """Authenticates an email by verifying the PGP signature. |
96 | |
97 | The mail is expected to be an ISignedMessage. |
98 | + |
99 | + raw_mail is just a byte string of the uninterpreted mail. |
100 | """ |
101 | |
102 | signature = mail.signature |
103 | @@ -89,6 +166,16 @@ |
104 | raise InactiveAccount( |
105 | "Mail from a user with an inactive account.") |
106 | |
107 | + dkim_result = _authenticateDkim(mail, raw_mail) |
108 | + |
109 | + if dkim_result: |
110 | + if mail.signature is not None: |
111 | + log.info('message has gpg signature, therefore not treating DKIM ' |
112 | + 'success as conclusive') |
113 | + else: |
114 | + setupInteraction(principal, email_addr) |
115 | + return principal |
116 | + |
117 | if signature is None: |
118 | # Mark the principal so that application code can check that the |
119 | # user was weakly authenticated. |
120 | @@ -255,7 +342,7 @@ |
121 | continue |
122 | |
123 | try: |
124 | - principal = authenticateEmail(mail) |
125 | + principal = authenticateEmail(mail, raw_mail) |
126 | except InvalidSignature, error: |
127 | _handle_user_error(error, mail) |
128 | continue |
129 | |
130 | === added file 'lib/lp/services/mail/tests/test_dkim.py' |
131 | --- lib/lp/services/mail/tests/test_dkim.py 1970-01-01 00:00:00 +0000 |
132 | +++ lib/lp/services/mail/tests/test_dkim.py 2010-07-12 20:52:02 +0000 |
133 | @@ -0,0 +1,258 @@ |
134 | +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
135 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
136 | + |
137 | +"""Test DKIM-signed messages""" |
138 | + |
139 | +__metaclass__ = type |
140 | + |
141 | +import logging |
142 | +import unittest |
143 | + |
144 | +from StringIO import StringIO |
145 | + |
146 | +import dkim |
147 | +import dns.resolver |
148 | + |
149 | +from zope.component import getUtility |
150 | + |
151 | +from canonical.launchpad.mail import ( |
152 | + incoming, |
153 | + signed_message_from_string, |
154 | + ) |
155 | +from canonical.launchpad.mail.incoming import ( |
156 | + authenticateEmail, ) |
157 | +from canonical.launchpad.interfaces.mail import IWeaklyAuthenticatedPrincipal |
158 | +from lp.testing import TestCaseWithFactory |
159 | +from canonical.testing.layers import DatabaseFunctionalLayer |
160 | + |
161 | + |
162 | +# sample private key made with 'openssl genrsa' and public key using 'openssl |
163 | +# rsa -pubout'. Not really the key for canonical.com ;-) |
164 | +sample_privkey = """\ |
165 | +-----BEGIN RSA PRIVATE KEY----- |
166 | +MIIBOwIBAAJBANmBe10IgY+u7h3enWTukkqtUD5PR52Tb/mPfjC0QJTocVBq6Za/ |
167 | +PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQJAYFUKsD+uMlcFu1D3YNaR |
168 | +EGYGXjJ6w32jYGJ/P072M3yWOq2S1dvDthI3nRT8MFjZ1wHDAYHrSpfDNJ3v2fvZ |
169 | +cQIhAPgRPmVYn+TGd59asiqG1SZqh+p+CRYHW7B8BsicG5t3AiEA4HYNOohlgWan |
170 | +8tKgqLJgUdPFbaHZO1nDyBgvV8hvWZUCIQDDdCq6hYKuKeYUy8w3j7cgJq3ih922 |
171 | +2qNWwdJCfCWQbwIgTY0cBvQnNe0067WQIpj2pG7pkHZR6qqZ9SE+AjNTHX0CIQCI |
172 | +Mgq55Y9MCq5wqzy141rnxrJxTwK9ABo3IAFMWEov3g== |
173 | +-----END RSA PRIVATE KEY----- |
174 | +""" |
175 | + |
176 | +sample_pubkey = """\ |
177 | +-----BEGIN PUBLIC KEY----- |
178 | +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T |
179 | +b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== |
180 | +-----END PUBLIC KEY----- |
181 | +""" |
182 | + |
183 | +sample_dns = """\ |
184 | +k=rsa; \ |
185 | +p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ |
186 | +b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" |
187 | + |
188 | + |
189 | +plain_content = """\ |
190 | +From: Foo Bar <foo.bar@canonical.com> |
191 | +Date: Fri, 1 Apr 2010 00:00:00 +1000 |
192 | +Subject: yet another comment |
193 | +To: 1@bugs.staging.launchpad.net |
194 | + |
195 | + importance critical |
196 | + |
197 | +Why isn't this fixed yet?""" |
198 | + |
199 | + |
200 | +class TestDKIM(TestCaseWithFactory): |
201 | + """Messages can be strongly authenticated by DKIM.""" |
202 | + |
203 | + layer = DatabaseFunctionalLayer |
204 | + |
205 | + def setUp(self): |
206 | + # Login with admin roles as we aren't testing access here. |
207 | + TestCaseWithFactory.setUp(self, 'admin@canonical.com') |
208 | + self._log_output = StringIO() |
209 | + handler = logging.StreamHandler(self._log_output) |
210 | + logger = logging.getLogger('mail-authenticate-dkim') |
211 | + logger.addHandler(handler) |
212 | + self.addCleanup(lambda: logger.removeHandler(handler)) |
213 | + self.monkeypatch_dns() |
214 | + |
215 | + def fake_signing(self, plain_message, canonicalize=None): |
216 | + if canonicalize is None: |
217 | + canonicalize = (dkim.Relaxed, dkim.Relaxed) |
218 | + dkim_line = dkim.sign(plain_message, |
219 | + selector='example', |
220 | + domain='canonical.com', |
221 | + privkey=sample_privkey, |
222 | + debuglog=self._log_output, |
223 | + canonicalize=canonicalize |
224 | + ) |
225 | + assert dkim_line[-1] == '\n' |
226 | + return dkim_line + plain_message |
227 | + |
228 | + def monkeypatch_dns(self): |
229 | + self._dns_responses = {} |
230 | + def my_lookup(name): |
231 | + try: |
232 | + return self._dns_responses[name] |
233 | + except KeyError: |
234 | + raise dns.resolver.NXDOMAIN() |
235 | + orig_dnstxt = dkim.dnstxt |
236 | + dkim.dnstxt = my_lookup |
237 | + def restore(): |
238 | + dkim.dnstxt = orig_dnstxt |
239 | + self.addCleanup(restore) |
240 | + |
241 | + def get_dkim_log(self): |
242 | + return self._log_output.getvalue() |
243 | + |
244 | + def assertStronglyAuthenticated(self, principal, signed_message): |
245 | + if IWeaklyAuthenticatedPrincipal.providedBy(principal): |
246 | + self.fail('expected strong authentication; got weak:\n' |
247 | + + self.get_dkim_log() + '\n\n' + signed_message) |
248 | + |
249 | + def assertWeaklyAuthenticated(self, principal, signed_message): |
250 | + if not IWeaklyAuthenticatedPrincipal.providedBy(principal): |
251 | + self.fail('expected weak authentication; got strong:\n' |
252 | + + self.get_dkim_log() + '\n\n' + signed_message) |
253 | + |
254 | + def assertDkimLogContains(self, substring): |
255 | + l = self.get_dkim_log() |
256 | + if l.find(substring) == -1: |
257 | + self.fail("didn't find %r in log: %s" % (substring, l)) |
258 | + |
259 | + def test_dkim_garbage_pubkey(self): |
260 | + signed_message = self.fake_signing(plain_content) |
261 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
262 | + 'aothuaonu' |
263 | + principal = authenticateEmail(signed_message_from_string(signed_message), |
264 | + signed_message) |
265 | + self.assertWeaklyAuthenticated(principal, signed_message) |
266 | + self.assertEqual(principal.person.preferredemail.email, |
267 | + 'foo.bar@canonical.com') |
268 | + self.assertDkimLogContains('invalid format in _domainkey txt record') |
269 | + |
270 | + def test_dkim_valid_strict(self): |
271 | + signed_message = self.fake_signing(plain_content, |
272 | + canonicalize=(dkim.Simple, dkim.Simple)) |
273 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
274 | + sample_dns |
275 | + principal = authenticateEmail(signed_message_from_string(signed_message), |
276 | + signed_message) |
277 | + self.assertStronglyAuthenticated(principal, signed_message) |
278 | + self.assertEqual(principal.person.preferredemail.email, |
279 | + 'foo.bar@canonical.com') |
280 | + |
281 | + def test_dkim_valid(self): |
282 | + signed_message = self.fake_signing(plain_content) |
283 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
284 | + sample_dns |
285 | + principal = authenticateEmail(signed_message_from_string(signed_message), |
286 | + signed_message) |
287 | + self.assertStronglyAuthenticated(principal, signed_message) |
288 | + self.assertEqual(principal.person.preferredemail.email, |
289 | + 'foo.bar@canonical.com') |
290 | + |
291 | + def test_dkim_untrusted_signer(self): |
292 | + # Valid signature from an untrusted domain -> untrusted |
293 | + signed_message = self.fake_signing(plain_content) |
294 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
295 | + sample_dns |
296 | + saved_domains = incoming._trusted_dkim_domains[:] |
297 | + def restore(): |
298 | + incoming._trusted_dkim_domains = saved_domains |
299 | + self.addCleanup(restore) |
300 | + incoming._trusted_dkim_domains = [] |
301 | + principal = authenticateEmail(signed_message_from_string(signed_message), |
302 | + signed_message) |
303 | + self.assertWeaklyAuthenticated(principal, signed_message) |
304 | + self.assertEqual(principal.person.preferredemail.email, |
305 | + 'foo.bar@canonical.com') |
306 | + |
307 | + def test_dkim_signing_irrelevant(self): |
308 | + # It's totally valid for a message to be signed by a domain other than |
309 | + # that of the From-sender, if that domain is relaying the message. |
310 | + # However, we shouldn't then trust the purported sender, because they |
311 | + # might have just made it up rather than relayed it. |
312 | + tweaked_message = plain_content.replace('foo.bar@canonical.com', |
313 | + 'steve.alexander@ubuntulinux.com') |
314 | + signed_message = self.fake_signing(tweaked_message) |
315 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
316 | + sample_dns |
317 | + principal = authenticateEmail(signed_message_from_string(signed_message), |
318 | + signed_message) |
319 | + self.assertWeaklyAuthenticated(principal, signed_message) |
320 | + # should come from From, not the dkim signature |
321 | + self.assertEqual(principal.person.preferredemail.email, |
322 | + 'steve.alexander@ubuntulinux.com') |
323 | + |
324 | + def test_dkim_changed_from_address(self): |
325 | + # if the address part of the message has changed, it's detected. we |
326 | + # still treat this as weakly authenticated by the purported From-header |
327 | + # sender, though perhaps in future we would prefer to reject these |
328 | + # messages. |
329 | + signed_message = self.fake_signing(plain_content) |
330 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
331 | + sample_dns |
332 | + fiddled_message = signed_message.replace('From: Foo Bar <foo.bar@canonical.com>', |
333 | + 'From: Carlos <carlos@canonical.com>') |
334 | + principal = authenticateEmail(signed_message_from_string(fiddled_message), |
335 | + fiddled_message) |
336 | + self.assertWeaklyAuthenticated(principal, fiddled_message) |
337 | + # should come from From, not the dkim signature |
338 | + self.assertEqual(principal.person.preferredemail.email, |
339 | + 'carlos@canonical.com') |
340 | + |
341 | + def test_dkim_changed_from_realname(self): |
342 | + # if the real name part of the message has changed, it's detected |
343 | + signed_message = self.fake_signing(plain_content) |
344 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
345 | + sample_dns |
346 | + fiddled_message = signed_message.replace('From: Foo Bar <foo.bar@canonical.com>', |
347 | + 'From: Evil Foo <foo.bar@canonical.com>') |
348 | + principal = authenticateEmail(signed_message_from_string(fiddled_message), |
349 | + fiddled_message) |
350 | + # we don't care about the real name for determining the principal |
351 | + self.assertWeaklyAuthenticated(principal, fiddled_message) |
352 | + self.assertEqual(principal.person.preferredemail.email, |
353 | + 'foo.bar@canonical.com') |
354 | + |
355 | + def test_dkim_nxdomain(self): |
356 | + # if there's no DNS entry for the pubkey |
357 | + # it should be handled decently |
358 | + signed_message = self.fake_signing(plain_content) |
359 | + principal = authenticateEmail(signed_message_from_string(signed_message), |
360 | + signed_message) |
361 | + self.assertWeaklyAuthenticated(principal, signed_message) |
362 | + self.assertEqual(principal.person.preferredemail.email, |
363 | + 'foo.bar@canonical.com') |
364 | + |
365 | + def test_dkim_message_unsigned(self): |
366 | + # degenerate case: no signature treated as weakly authenticated |
367 | + principal = authenticateEmail(signed_message_from_string(plain_content), |
368 | + plain_content) |
369 | + self.assertWeaklyAuthenticated(principal, plain_content) |
370 | + self.assertEqual(principal.person.preferredemail.email, |
371 | + 'foo.bar@canonical.com') |
372 | + # the library doesn't log anything if there's no header at all |
373 | + |
374 | + def test_dkim_body_mismatch(self): |
375 | + # The message message has a syntactically valid DKIM signature that |
376 | + # doesn't actually correspond to what was signed. We log something about |
377 | + # this but we don't want to drop the message. |
378 | + signed_message = self.fake_signing(plain_content) |
379 | + signed_message += 'blah blah' |
380 | + self._dns_responses['example._domainkey.canonical.com.'] = \ |
381 | + sample_dns |
382 | + principal = authenticateEmail(signed_message_from_string(signed_message), |
383 | + signed_message) |
384 | + self.assertWeaklyAuthenticated(principal, signed_message) |
385 | + self.assertEqual(principal.person.preferredemail.email, |
386 | + 'foo.bar@canonical.com') |
387 | + self.assertDkimLogContains('body hash mismatch') |
388 | + |
389 | + |
390 | +def test_suite(): |
391 | + return unittest.TestLoader().loadTestsFromName(__name__) |
392 | |
393 | === modified file 'lib/lp/services/mail/tests/test_signedmessage.py' |
394 | --- lib/lp/services/mail/tests/test_signedmessage.py 2009-06-30 16:56:07 +0000 |
395 | +++ lib/lp/services/mail/tests/test_signedmessage.py 2010-07-12 20:52:02 +0000 |
396 | @@ -28,7 +28,7 @@ |
397 | from canonical.testing.layers import DatabaseFunctionalLayer |
398 | |
399 | class TestSignedMessage(TestCaseWithFactory): |
400 | - """Test SignedMessage class correctly extracts the GPG signatures.""" |
401 | + """Test SignedMessage class correctly extracts and verifies the GPG signatures.""" |
402 | |
403 | layer = DatabaseFunctionalLayer |
404 | |
405 | @@ -45,7 +45,7 @@ |
406 | msg = signed_message_from_string(email_message.as_string()) |
407 | self.assertIs(None, msg.signedContent) |
408 | self.assertIs(None, msg.signature) |
409 | - principle = authenticateEmail(msg) |
410 | + principle = authenticateEmail(msg, msg.as_string()) |
411 | self.assertEqual(sender, principle.person) |
412 | self.assertTrue( |
413 | IWeaklyAuthenticatedPrincipal.providedBy(principle)) |
414 | @@ -73,7 +73,7 @@ |
415 | # the principle is set to the sender, but weakly authenticated. |
416 | sender = self.factory.makePerson() |
417 | msg = self._get_clearsigned_for_person(sender) |
418 | - principle = authenticateEmail(msg) |
419 | + principle = authenticateEmail(msg, msg.as_string()) |
420 | self.assertIsNot(None, msg.signature) |
421 | self.assertEqual(sender, principle.person) |
422 | self.assertTrue( |
423 | @@ -83,7 +83,7 @@ |
424 | # The test keys belong to Sample Person. |
425 | sender = getUtility(IPersonSet).getByEmail('test@canonical.com') |
426 | msg = self._get_clearsigned_for_person(sender) |
427 | - principle = authenticateEmail(msg) |
428 | + principle = authenticateEmail(msg, msg.as_string()) |
429 | self.assertIsNot(None, msg.signature) |
430 | self.assertEqual(sender, principle.person) |
431 | self.assertFalse( |
432 | @@ -127,7 +127,7 @@ |
433 | # the principle is set to the sender, but weakly authenticated. |
434 | sender = self.factory.makePerson() |
435 | msg = self._get_detached_message_for_person(sender) |
436 | - principle = authenticateEmail(msg) |
437 | + principle = authenticateEmail(msg, msg.as_string()) |
438 | self.assertIsNot(None, msg.signature) |
439 | self.assertEqual(sender, principle.person) |
440 | self.assertTrue( |
441 | @@ -137,7 +137,7 @@ |
442 | # Test a detached correct signature. |
443 | sender = getUtility(IPersonSet).getByEmail('test@canonical.com') |
444 | msg = self._get_detached_message_for_person(sender) |
445 | - principle = authenticateEmail(msg) |
446 | + principle = authenticateEmail(msg, msg.as_string()) |
447 | self.assertIsNot(None, msg.signature) |
448 | self.assertEqual(sender, principle.person) |
449 | self.assertFalse( |
450 | |
451 | === modified file 'setup.py' |
452 | --- setup.py 2010-07-06 08:58:00 +0000 |
453 | +++ setup.py 2010-07-12 20:52:02 +0000 |
454 | @@ -29,6 +29,8 @@ |
455 | 'chameleon.core', |
456 | 'chameleon.zpt', |
457 | 'cssutils', |
458 | + # Required for pydkim |
459 | + 'dnspython', |
460 | 'feedvalidator', |
461 | 'funkload', |
462 | 'launchpadlib', |
463 | @@ -51,6 +53,7 @@ |
464 | 'paramiko', |
465 | 'python-memcached', |
466 | 'pyasn1', |
467 | + 'pydkim', |
468 | 'python-openid', |
469 | 'pytz', |
470 | # This appears to be a broken indirect dependency from zope.security: |
471 | |
472 | === modified file 'versions.cfg' |
473 | --- versions.cfg 2010-07-09 15:10:07 +0000 |
474 | +++ versions.cfg 2010-07-12 20:52:02 +0000 |
475 | @@ -13,6 +13,8 @@ |
476 | # Required by Windmill to run on 2.4 |
477 | ctypes = 1.0.2 |
478 | docutils = 0.5 |
479 | +# Required by pydkim |
480 | +dnspython = 1.7.1 |
481 | elementtree = 1.2.6-20050316 |
482 | epydoc = 3.0.1 |
483 | feedvalidator = 0.0.0DEV-r1049 |
484 | @@ -47,6 +49,7 @@ |
485 | PasteDeploy = 1.3.3 |
486 | pyasn1 = 0.0.9a |
487 | pycrypto = 2.0.1 |
488 | +pydkim = 0.3-bug-587783-2 |
489 | pyOpenSSL = 0.10 |
490 | python-memcached = 1.45 |
491 | python-openid = 2.2.1 |
There is an upstream bug in python-dkim, https:/ /bugs.edge. launchpad. net/ubuntu/ +source/ pydkim/ +bug/587783