← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~mbp/launchpad/925597-dkim-from into lp:launchpad

 

Martin Pool has proposed merging lp:~mbp/launchpad/925597-dkim-from into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #925597 in Launchpad itself: "From address is ignored if DKIM is supported"
  https://bugs.launchpad.net/launchpad/+bug/925597

For more details, see:
https://code.launchpad.net/~mbp/launchpad/925597-dkim-from/+merge/98976

This fixes bug <https://bugs.launchpad.net/launchpad/+bug/925597> which is hit if you are sending mail from gmail, but your gmail address is not registered with lp.

I wonder if I also need to test the case that your gmail address is known but not verified?  I probably should.
-- 
https://code.launchpad.net/~mbp/launchpad/925597-dkim-from/+merge/98976
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~mbp/launchpad/925597-dkim-from into lp:launchpad.
=== modified file 'lib/lp/services/mail/incoming.py'
--- lib/lp/services/mail/incoming.py	2012-01-04 03:23:19 +0000
+++ lib/lp/services/mail/incoming.py	2012-03-23 07:14:26 +0000
@@ -103,8 +103,8 @@
     return domain in _trusted_dkim_domains
 
 
-def _authenticateDkim(signed_message):
-    """Attempt DKIM authentication of email.
+def _verifyDkimOrigin(signed_message):
+    """Find a From or Sender address for which there's a DKIM signature, if any.
 
     :returns: A string email address for the trusted sender, if there is one,
     otherwise None.
@@ -190,6 +190,34 @@
         return None
 
 
+def _getPrincipalByDkim(mail):
+    """Determine the security principal from DKIM, if possible.
+
+    To qualify:
+        * there must be a dkim signature from a trusted domain
+        * the From or Sender must be in that domain
+        * the address in this header must be verified for a person
+
+    :returns: (None, None), or (principal, trusted_addr).
+    """
+    log = logging.getLogger('mail-authenticate-dkim')
+    authutil = getUtility(IPlacelessAuthUtility)
+
+    dkim_trusted_address = _verifyDkimOrigin(mail)
+    if dkim_trusted_address is None:
+        return None, None
+
+    log.debug('authenticated DKIM mail origin %s' % dkim_trusted_address)
+    dkim_principal = authutil.getPrincipalByLogin(dkim_trusted_address)
+    if dkim_principal is None:
+        log.debug("valid dkim signature, but not from a known email address, "
+            "therefore disregarding it")
+        return None, None
+    else:
+        return (dkim_principal, dkim_trusted_address)
+
+
+
 def authenticateEmail(mail,
     signature_timestamp_checker=None):
     """Authenticates an email by verifying the PGP signature.
@@ -207,20 +235,13 @@
     """
 
     log = logging.getLogger('process-mail')
-
-    dkim_trusted_addr = _authenticateDkim(mail)
-    if dkim_trusted_addr is not None:
-        # The Sender field, if signed by a trusted domain, is the strong
-        # authenticator for this mail.
-        log.debug('trusted DKIM mail from %s' % dkim_trusted_addr)
-        email_addr = dkim_trusted_addr
-    else:
-        email_addr = parseaddr(mail['From'])[1]
-
     authutil = getUtility(IPlacelessAuthUtility)
-    principal = authutil.getPrincipalByLogin(email_addr)
-
-    # Check that sender is registered in Launchpad and the email is signed.
+
+    principal, dkim_trusted_address = _getPrincipalByDkim(mail)
+    if dkim_trusted_address is None:
+        from_addr = parseaddr(mail['From'])[1]
+        principal = authutil.getPrincipalByLogin(from_addr)
+
     if principal is None:
         setupInteraction(authutil.unauthenticatedPrincipal())
         return None
@@ -230,9 +251,9 @@
         raise InactiveAccount(
             "Mail from a user with an inactive account.")
 
-    if dkim_trusted_addr is not None:
+    if dkim_trusted_address:
         log.debug('accepting dkim strongly authenticated mail')
-        setupInteraction(principal, dkim_trusted_addr)
+        setupInteraction(principal, dkim_trusted_address)
         return principal
     else:
         log.debug("attempt gpg authentication for %r" % person)

=== modified file 'lib/lp/services/mail/tests/test_dkim.py'
--- lib/lp/services/mail/tests/test_dkim.py	2012-01-01 02:58:52 +0000
+++ lib/lp/services/mail/tests/test_dkim.py	2012-03-23 07:14:26 +0000
@@ -46,16 +46,6 @@
 b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ=="""
 
 
-plain_content = """\
-From: Foo Bar <foo.bar@xxxxxxxxxxxxx>
-Date: Fri, 1 Apr 2010 00:00:00 +1000
-Subject: yet another comment
-To: 1@xxxxxxxxxxxxxxxxxxxxxxxxxx
-
-  importance critical
-
-Why isn't this fixed yet?"""
-
 
 class TestDKIM(TestCaseWithFactory):
     """Messages can be strongly authenticated by DKIM."""
@@ -119,13 +109,29 @@
         if l.find(substring) == -1:
             self.fail("didn't find %r in log: %s" % (substring, l))
 
+    def makeMessageText(self, sender=None, from_address=None):
+        if from_address is None:
+            from_address = "Foo Bar <foo.bar@xxxxxxxxxxxxx>"
+        text = ("From: " + from_address + "\n" + """\
+Date: Fri, 1 Apr 2010 00:00:00 +1000
+Subject: yet another comment
+To: 1@xxxxxxxxxxxxxxxxxxxxxxxxxx
+
+  importance critical
+
+Why isn't this fixed yet?""")
+        if sender is not None:
+            text = "Sender: " + sender + "\n" + text
+        return text
+
+
     def test_dkim_broken_pubkey(self):
         """Handle a subtly-broken pubkey like qq.com, see bug 881237.
 
         The message is not trusted but inbound message processing does not
         abort either.
         """
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns.replace(';', '')
         principal = authenticateEmail(
@@ -136,7 +142,7 @@
         self.assertDkimLogContains('unexpected error in DKIM verification')
 
     def test_dkim_garbage_pubkey(self):
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         self._dns_responses['example._domainkey.canonical.com.'] = \
             'aothuaonu'
         principal = authenticateEmail(
@@ -155,7 +161,7 @@
             self.test_dkim_valid_strict)
 
     def test_dkim_valid_strict(self):
-        signed_message = self.fake_signing(plain_content,
+        signed_message = self.fake_signing(self.makeMessageText(),
             canonicalize=(dkim.Simple, dkim.Simple))
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns
@@ -166,7 +172,7 @@
             'foo.bar@xxxxxxxxxxxxx')
 
     def test_dkim_valid(self):
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns
         principal = authenticateEmail(
@@ -177,7 +183,7 @@
 
     def test_dkim_untrusted_signer(self):
         # Valid signature from an untrusted domain -> untrusted
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns
         saved_domains = incoming._trusted_dkim_domains[:]
@@ -198,8 +204,8 @@
         # that of the From-sender, if that domain is relaying the message.
         # However, we shouldn't then trust the purported sender, because they
         # might have just made it up rather than relayed it.
-        tweaked_message = plain_content.replace('foo.bar@xxxxxxxxxxxxx',
-            'steve.alexander@xxxxxxxxxxxxxxx')
+        tweaked_message = self.makeMessageText(
+            from_address='steve.alexander@xxxxxxxxxxxxxxx')
         signed_message = self.fake_signing(tweaked_message)
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns
@@ -215,7 +221,7 @@
         #  We still treat this as weakly authenticated by the purported
         # From-header sender, though perhaps in future we would prefer
         # to reject these messages.
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns
         fiddled_message = signed_message.replace(
@@ -230,7 +236,7 @@
 
     def test_dkim_changed_from_realname(self):
         # If the real name part of the message has changed, it's detected.
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns
         fiddled_message = signed_message.replace(
@@ -246,7 +252,7 @@
     def test_dkim_nxdomain(self):
         # If there's no DNS entry for the pubkey it should be handled
         # decently.
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         principal = authenticateEmail(
             signed_message_from_string(signed_message))
         self.assertWeaklyAuthenticated(principal, signed_message)
@@ -258,8 +264,8 @@
         # treated as weakly authenticated.
         # The library doesn't log anything if there's no header at all.
         principal = authenticateEmail(
-            signed_message_from_string(plain_content))
-        self.assertWeaklyAuthenticated(principal, plain_content)
+            signed_message_from_string(self.makeMessageText()))
+        self.assertWeaklyAuthenticated(principal, self.makeMessageText())
         self.assertEqual(principal.person.preferredemail.email,
             'foo.bar@xxxxxxxxxxxxx')
 
@@ -267,7 +273,7 @@
         # The message has a syntactically valid DKIM signature that
         # doesn't actually correspond to what was signed.  We log
         # something about this but we don't want to drop the message.
-        signed_message = self.fake_signing(plain_content)
+        signed_message = self.fake_signing(self.makeMessageText())
         signed_message += 'blah blah'
         self._dns_responses['example._domainkey.canonical.com.'] = \
             sample_dns
@@ -292,11 +298,32 @@
             person=person,
             address='dkimtest@xxxxxxxxxxx')
         self._dns_responses['example._domainkey.canonical.com.'] = sample_dns
-        tweaked_message = (
-            "Sender: dkimtest@xxxxxxxxxxxxx\n" + plain_content.replace(
-                "From: Foo Bar <foo.bar@xxxxxxxxxxxxx>",
-                "From: DKIM Test <dkimtest@xxxxxxxxxxx>"))
+        tweaked_message = self.makeMessageText(
+            sender="dkimtest@xxxxxxxxxxxxx",
+            from_address="DKIM Test <dkimtest@xxxxxxxxxxx>")
         signed_message = self.fake_signing(tweaked_message)
         principal = authenticateEmail(
             signed_message_from_string(signed_message))
         self.assertStronglyAuthenticated(principal, signed_message)
+
+    def test_dkim_signed_but_from_unknown_address(self):
+        """Sent from trusted dkim address, but only the From address is known.
+        
+        See https://bugs.launchpad.net/launchpad/+bug/925597
+        """
+        person = self.factory.makePerson(
+            email='dkimtest@xxxxxxxxxxx',
+            name='dkimtest',
+            displayname='DKIM Test')
+        self._dns_responses['example._domainkey.canonical.com.'] = sample_dns
+        tweaked_message = self.makeMessageText(
+            sender="dkimtest@xxxxxxxxxxxxx",
+            from_address="DKIM Test <dkimtest@xxxxxxxxxxx>")
+        signed_message = self.fake_signing(tweaked_message)
+        principal = authenticateEmail(
+            signed_message_from_string(signed_message))
+        self.assertEqual(principal.person.preferredemail.email,
+            'dkimtest@xxxxxxxxxxx')
+        self.assertWeaklyAuthenticated(principal, signed_message)
+        self.assertDkimLogContains(
+            'valid dkim signature, but not from a known email address')