launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02017
[Merge] lp:~mbp/launchpad/dkim into lp:launchpad
Martin Pool has proposed merging lp:~mbp/launchpad/dkim into lp:launchpad.
Requested reviews:
Jonathan Lange (jml)
Launchpad code reviewers (launchpad-reviewers)
Graham Binns (gmb): code
Related bugs:
#316272 launchpad should verify gmail or DomainKeys authenticators
https://bugs.launchpad.net/bugs/316272
#643200 check for stale gpg signatures only cover malone
https://bugs.launchpad.net/bugs/643200
#643219 dkim-signed mail to new@bugs doesn't work
https://bugs.launchpad.net/bugs/643219
This fixes a few more things related to authentication of incoming mail:
* gpg signature timestamp checks should cover all mail, not just that to malone (bug 643200)
* checks on mail to new@ should make sure it's strongly authenticated, not specifically that it has a gpg signature (bug 643219)
* there was no test that gpg mail with implausible timestamps was actually rejected afaics
I haven't interactively tested this and I'm not utterly confident in the existing test coverage, so please review carefully and test it interactively yourself if you know how.
MaloneHandler.process was a bit large so I split out the code that decides what if any commands will be executed.
Thanks
--
https://code.launchpad.net/~mbp/launchpad/dkim/+merge/41819
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~mbp/launchpad/dkim into lp:launchpad.
=== modified file 'lib/canonical/launchpad/doc/emailauthentication.txt'
--- lib/canonical/launchpad/doc/emailauthentication.txt 2010-10-18 22:24:59 +0000
+++ lib/canonical/launchpad/doc/emailauthentication.txt 2010-11-25 04:47:03 +0000
@@ -20,12 +20,18 @@
>>> 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 canonical.launchpad.mail.ftests import read_test_message
>>> msg = read_test_message('signed_detached.txt')
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
If the user isn't registered in Launchpad, None is return, if it
succeeds the authenticated principal:
@@ -52,7 +58,7 @@
message. Inline signatures are supported as well.
>>> msg = read_test_message('signed_inline.txt')
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> principal is not None
True
>>> name, addr = email.Utils.parseaddr(msg['From'])
@@ -65,7 +71,7 @@
As well as signed multipart messages:
>>> msg = read_test_message('signed_multipart.txt')
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> principal is not None
True
>>> name, addr = email.Utils.parseaddr(msg['From'])
@@ -80,7 +86,7 @@
to deal with it if we receive a dash escaped message.
>>> msg = read_test_message('signed_dash_escaped.txt')
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> principal is not None
True
>>> name, addr = email.Utils.parseaddr(msg['From'])
@@ -96,7 +102,7 @@
>>> 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)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
Traceback (most recent call last):
...
InvalidSignature:...
@@ -131,7 +137,7 @@
... msg = email.message_from_string(
... line_ending.join(msg_lines), _class=SignedMessage)
... msg.parsed_string = msg.as_string()
- ... principal = authenticateEmail(msg)
+ ... principal = authenticateEmail(msg, accept_any_timestamp)
... authenticated_person = IPerson(principal)
... print authenticated_person.preferredemail.email
test@xxxxxxxxxxxxx
@@ -156,7 +162,7 @@
Content-Type: multipart/mixed; boundary="--------------------EuxKj2iCbKjpUGkD"
...
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> print IPerson(principal).displayname
Sample Person
@@ -178,7 +184,7 @@
>>> from canonical.launchpad.interfaces.mail import (
... IWeaklyAuthenticatedPrincipal)
>>> msg = read_test_message('unsigned_multipart.txt')
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
True
@@ -191,7 +197,7 @@
authenticated user:
>>> msg = read_test_message('signed_key_not_registered.txt')
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
True
@@ -205,7 +211,7 @@
principal.
>>> msg = read_test_message('signed_inline.txt')
- >>> principal = authenticateEmail(msg)
+ >>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
False
=== renamed file 'lib/canonical/launchpad/mail/errortemplates/not-gpg-signed.txt' => 'lib/canonical/launchpad/mail/errortemplates/unauthenticated-bug-creation.txt'
=== modified file 'lib/canonical/launchpad/mail/handlers.py'
--- lib/canonical/launchpad/mail/handlers.py 2010-11-08 13:11:30 +0000
+++ lib/canonical/launchpad/mail/handlers.py 2010-11-25 04:47:03 +0000
@@ -15,7 +15,6 @@
from canonical.config import config
from canonical.database.sqlbase import rollback
from canonical.launchpad.helpers import get_email_template
-from canonical.launchpad.interfaces.gpghandler import IGPGHandler
from canonical.launchpad.interfaces.mail import (
EmailProcessingError,
IBugEditEmailCommand,
@@ -31,7 +30,6 @@
)
from canonical.launchpad.mail.helpers import (
ensure_not_weakly_authenticated,
- ensure_sane_signature_timestamp,
get_main_body,
guess_bugtask,
IncomingEmailError,
@@ -61,16 +59,6 @@
)
-def extract_signature_timestamp(signed_msg):
- # break import cycle
- from lp.services.mail.incoming import (
- canonicalise_line_endings)
- signature = getUtility(IGPGHandler).getVerifiedSignature(
- canonicalise_line_endings(signed_msg.signedContent),
- signed_msg.signature)
- return signature.timestamp
-
-
class MaloneHandler:
"""Handles emails sent to Malone.
@@ -90,47 +78,64 @@
name, args in parse_commands(content,
BugEmailCommands.names())]
- def process(self, signed_msg, to_addr, filealias=None, log=None,
- extract_signature_timestamp=extract_signature_timestamp):
- """See IMailHandler."""
+ def extractAndAuthenticateCommands(self, signed_msg, to_addr):
+ """Extract commands and handle special destinations.
+
+ NB: The authentication is carried out against the current principal,
+ not directly against the message. authenticateEmail must previously
+ have been called on this thread.
+
+ :returns: (final_result, add_comment_to_bug, commands)
+ If final_result is non-none, stop processing and return this value
+ to indicate whether the message was dealt with or not.
+ If add_comment_to_bug, add the contents to the first bug
+ selected.
+ commands is a list of bug commands.
+ """
+ CONTEXT = 'bug report'
commands = self.getCommands(signed_msg)
- user, host = to_addr.split('@')
+ to_user, to_host = to_addr.split('@')
add_comment_to_bug = False
- signature = signed_msg.signature
+ # If there are any commands, we must have strong authentication.
+ # We send a different failure message for attempts to create a new
+ # bug.
+ if to_user.lower() == 'new':
+ ensure_not_weakly_authenticated(signed_msg, CONTEXT,
+ 'unauthenticated-bug-creation.txt')
+ elif len(commands) > 0:
+ ensure_not_weakly_authenticated(signed_msg, CONTEXT)
+ if to_user.lower() == 'new':
+ commands.insert(0, BugEmailCommands.get('bug', ['new']))
+ elif to_user.isdigit():
+ # A comment to a bug. We set add_comment_to_bug to True so
+ # that the comment gets added to the bug later. We don't add
+ # the comment now, since we want to let the 'bug' command
+ # handle the possible errors that can occur while getting
+ # the bug.
+ add_comment_to_bug = True
+ commands.insert(0, BugEmailCommands.get('bug', [to_user]))
+ elif to_user.lower() == 'help':
+ from_user = getUtility(ILaunchBag).user
+ if from_user is not None:
+ preferredemail = from_user.preferredemail
+ if preferredemail is not None:
+ to_address = str(preferredemail.email)
+ self.sendHelpEmail(to_address)
+ return True, False, None
+ elif to_user.lower() != 'edit':
+ # Indicate that we didn't handle the mail.
+ return False, False, None
+ return None, add_comment_to_bug, commands
+
+ def process(self, signed_msg, to_addr, filealias=None, log=None):
+ """See IMailHandler."""
try:
- if len(commands) > 0:
- CONTEXT = 'bug report'
- ensure_not_weakly_authenticated(signed_msg, CONTEXT)
- if signature is not None:
- ensure_sane_signature_timestamp(
- extract_signature_timestamp(signed_msg), CONTEXT)
-
- if user.lower() == 'new':
- # A submit request.
- commands.insert(0, BugEmailCommands.get('bug', ['new']))
- if signature is None:
- raise IncomingEmailError(
- get_error_message('not-gpg-signed.txt'))
- elif user.isdigit():
- # A comment to a bug. We set add_comment_to_bug to True so
- # that the comment gets added to the bug later. We don't add
- # the comment now, since we want to let the 'bug' command
- # handle the possible errors that can occur while getting
- # the bug.
- add_comment_to_bug = True
- commands.insert(0, BugEmailCommands.get('bug', [user]))
- elif user.lower() == 'help':
- from_user = getUtility(ILaunchBag).user
- if from_user is not None:
- preferredemail = from_user.preferredemail
- if preferredemail is not None:
- to_address = str(preferredemail.email)
- self.sendHelpEmail(to_address)
- return True
- elif user.lower() != 'edit':
- # Indicate that we didn't handle the mail.
- return False
+ (final_result, add_comment_to_bug,
+ commands, ) = self.extractAndAuthenticateCommands(
+ signed_msg, to_addr)
+ if final_result is not None:
+ return final_result
bug = None
bug_event = None
=== modified file 'lib/canonical/launchpad/mail/helpers.py'
--- lib/canonical/launchpad/mail/helpers.py 2010-11-08 13:11:30 +0000
+++ lib/canonical/launchpad/mail/helpers.py 2010-11-25 04:47:03 +0000
@@ -211,7 +211,14 @@
def ensure_not_weakly_authenticated(signed_msg, context,
error_template='not-signed.txt',
no_key_template='key-not-registered.txt'):
- """Make sure that the current principal is not weakly authenticated."""
+ """Make sure that the current principal is not weakly authenticated.
+
+ NB: While handling an email, the authentication state is stored partly in
+ properties of the message object, and partly in the current security
+ principal. As a consequence this function will only work correctly if the
+ message has just been passed through authenticateEmail -- you can't give
+ it an arbitrary message object.
+ """
cur_principal = get_current_principal()
# The security machinery doesn't know about
# IWeaklyAuthenticatedPrincipal yet, so do a manual
@@ -232,7 +239,18 @@
def ensure_sane_signature_timestamp(timestamp, context,
error_template='old-signature.txt'):
- """Ensure the signature was generated recently but not in the future."""
+ """Ensure the signature was generated recently but not in the future.
+
+ If the timestamp is wrong, the message is rejected rather than just
+ treated as untrusted, so that the user gets a chance to understand
+ the problem. Therefore, this raises an error rather than returning
+ a value.
+
+ :param context: Short user-readable description of the place
+ the problem occurred.
+ :raises IncomingEmailError: if the timestamp is stale or implausible,
+ containing a message based on the context and template.
+ """
fourty_eight_hours = 48 * 60 * 60
ten_minutes = 10 * 60
now = time.time()
=== modified file 'lib/canonical/launchpad/mail/tests/test_handlers.py'
--- lib/canonical/launchpad/mail/tests/test_handlers.py 2010-10-26 15:47:24 +0000
+++ lib/canonical/launchpad/mail/tests/test_handlers.py 2010-11-25 04:47:03 +0000
@@ -6,6 +6,7 @@
from doctest import DocTestSuite
import email
import time
+import transaction
import unittest
from canonical.database.sqlbase import commit
@@ -42,6 +43,66 @@
self.assertEqual('bug', commands[0].name)
self.assertEqual(['foo'], commands[0].string_args)
+ def test_NonGPGAuthenticatedNewBug(self):
+ """Mail authenticated other than by gpg can create bugs.
+
+ The incoming mail layer is responsible for authenticating the mail,
+ and setting the current principal to the sender of the mail, either
+ weakly or non-weakly authenticated. At the layer of the handler,
+ which this class is testing, we shouldn't care by what mechanism we
+ decided to act on behalf of the mail sender, only that we did.
+
+ In bug 643219, Launchpad had a problem where the MaloneHandler code
+ was puncturing that abstraction and directly looking at the GPG
+ signature; this test checks it's fixed.
+ """
+ # NB SignedMessage by default isn't actually signed, it just has the
+ # capability of knowing about signing.
+ message = self.factory.makeSignedMessage(body=' affects malone\nhi!')
+ self.assertEquals(message.signature, None)
+
+ # Pretend that the mail auth has given us a logged-in user.
+ handler = MaloneHandler()
+ with person_logged_in(self.factory.makePerson()):
+ mail_handled, add_comment_to_bug, commands = \
+ handler.extractAndAuthenticateCommands(message,
+ 'new@xxxxxxxxxxxxxxxxxx')
+ self.assertEquals(mail_handled, None)
+ self.assertEquals(map(str, commands), [
+ 'bug new',
+ 'affects malone',
+ ])
+
+ def test_mailToHelpFromUnknownUser(self):
+ """Mail from people of no account to help@ is simply dropped.
+ """
+ message = self.factory.makeSignedMessage()
+ handler = MaloneHandler()
+ mail_handled, add_comment_to_bug, commands = \
+ handler.extractAndAuthenticateCommands(message,
+ 'help@xxxxxxxxxxxxxxxxxx')
+ self.assertEquals(mail_handled, True)
+ self.assertEquals(self.getSentMail(), [])
+
+ def test_mailToHelp(self):
+ """Mail to help@ generates a help command."""
+ message = self.factory.makeSignedMessage()
+ handler = MaloneHandler()
+ with person_logged_in(self.factory.makePerson()):
+ mail_handled, add_comment_to_bug, commands = \
+ handler.extractAndAuthenticateCommands(message,
+ 'help@xxxxxxxxxxxxxxxxxx')
+ self.assertEquals(mail_handled, True)
+ self.assertEquals(len(self.getSentMail()), 1)
+ # TODO: Check the right mail was sent. -- mbp 20100923
+
+ def getSentMail(self):
+ # Sending mail is (unfortunately) a side effect of parsing the
+ # commands, and unfortunately you must commit the transaction to get
+ # them sent.
+ transaction.commit()
+ return stub.test_emails[:]
+
class FakeSignature:
=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-11-05 14:17:11 +0000
+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-11-25 04:47:03 +0000
@@ -50,6 +50,8 @@
signed, so that the system can verify the sender. But to avoid having
to sign each email, we'll create a class which fakes a signed email:
+ >>> from lp.testing import sampledata
+
>>> import email.Message
>>> class MockSignedMessage(email.Message.Message):
... def __init__(self, *args, **kws):
@@ -77,14 +79,10 @@
... msg['Message-Id'] = factory.makeUniqueRFC822MsgId()
... return msg
- >>> import time
- >>> def fake_extract_signature_timestamp(signed_msg):
- ... return time.time()
-
>>> def process_email(raw_mail):
... msg = construct_email(raw_mail)
... handler.process(msg, msg['To'],
- ... extract_signature_timestamp=fake_extract_signature_timestamp)
+ ... )
>>> process_email(submit_mail)
@@ -156,7 +154,7 @@
If we would file a bug on Ubuntu instead, we would submit a mail like
this:
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
... To: new@xxxxxxxxxxxxxxxxxx
... Date: Fri Jun 17 10:20:23 BST 2005
@@ -342,13 +340,15 @@
>>> from canonical.launchpad.interfaces.mail import IWeaklyAuthenticatedPrincipal
>>> from zope.interface import directlyProvides, directlyProvidedBy
>>> from zope.security.management import queryInteraction
- >>> participations = queryInteraction().participations
- >>> len(participations)
- 1
- >>> current_principal = participations[0].principal
- >>> directlyProvides(
- ... current_principal, directlyProvidedBy(current_principal),
- ... IWeaklyAuthenticatedPrincipal)
+
+ >>> def simulate_receiving_untrusted_mail():
+ ... participations = queryInteraction().participations
+ ... assert len(participations) == 1
+ ... current_principal = participations[0].principal
+ ... directlyProvides(
+ ... current_principal, directlyProvidedBy(current_principal),
+ ... IWeaklyAuthenticatedPrincipal)
+ >>> simulate_receiving_untrusted_mail()
Now we send a comment containing commands.
@@ -385,6 +385,8 @@
>>> def print_latest_email():
... commit()
+ ... if not stub.test_emails:
+ ... raise AssertionError("No emails queued!")
... from_addr, to_addrs, raw_message = stub.test_emails[-1]
... sent_msg = email.message_from_string(raw_message)
... error_mail, original_mail = sent_msg.get_payload()
@@ -411,7 +413,7 @@
... comment_mail, _class=MockUnsignedMessage)
>>> handler.process(
... msg, msg['To'],
- ... extract_signature_timestamp=fake_extract_signature_timestamp)
+ ... )
True
>>> commit()
@@ -457,12 +459,9 @@
>>> added_message in bug_one.messages
True
-Unmark the principal:
+In these tests, every time we log in, we're fully trusted again:
- >>> provided_interfaces = directlyProvidedBy(current_principal)
- >>> directlyProvides(
- ... current_principal,
- ... provided_interfaces - IWeaklyAuthenticatedPrincipal)
+ >>> login(sampledata.USER_EMAIL)
Commands
@@ -485,7 +484,7 @@
>>> def submit_command_email(msg):
... handler.process(
... msg, msg['To'],
- ... extract_signature_timestamp=fake_extract_signature_timestamp)
+ ... )
... commit()
... sync(bug)
@@ -734,7 +733,7 @@
>>> 'Foo Bar' in [subscription.person.displayname
... for subscription in bug_four.subscriptions]
False
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> submit_commands(bug_four, 'unsubscribe')
>>> 'Sample Person' in [subscription.person.displayname
... for subscription in bug_four.subscriptions]
@@ -793,9 +792,9 @@
... for subscriber in bug_five.getIndirectSubscribers()])
[u'Sample Person', u'Ubuntu Team']
-(Log back in as test@xxxxxxxxxxxxx for the tests that follow.)
+(Log back in for the tests that follow.)
- >>> login("test@xxxxxxxxxxxxx")
+ >>> login(sampledata.USER_EMAIL)
If we specify a non-existant user, an error message will be sent:
@@ -1108,7 +1107,7 @@
Attempting to set the milestone for a bug without sufficient
permissions also elicits an error message:
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> bug = new_firefox_bug()
>>> commit()
@@ -1144,13 +1143,13 @@
>>>
>>> login(ADMIN_EMAIL)
>>> sample_person = getUtility(IPersonSet).getByEmail(
- ... "test@xxxxxxxxxxxxx")
+ ... sampledata.USER_EMAIL)
>>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
>>> ubuntu = removeSecurityProxy(ubuntu)
>>> ubuntu.bug_supervisor = sample_person
>>> logout()
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
Like the web UI, we can assign a bug to nobody.
@@ -1246,10 +1245,10 @@
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> ubuntu.driver = getUtility(IPersonSet).getByEmail(
- ... 'test@xxxxxxxxxxxxx')
+ ... sampledata.USER_EMAIL)
>>> commit()
>>> LaunchpadZopelessLayer.switchDbUser(config.processmail.dbuser)
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> sync(bug)
Now a new bugtask for the series will be create directly.
@@ -1302,7 +1301,7 @@
>>> ubuntu.driver = None
>>> commit()
>>> LaunchpadZopelessLayer.switchDbUser(config.processmail.dbuser)
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> bug = new_firefox_bug()
>>> for bugtask in bug.bugtasks:
@@ -1329,7 +1328,7 @@
... print driver.displayname
Sample Person
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> submit_commands(bug, 'affects /firefox/trunk')
>>> for bugtask in bug.bugtasks:
@@ -1367,7 +1366,7 @@
... print nomination.target.bugtargetdisplayname
Evolution trunk
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
Let's take on the upstream task on bug four as well. This time we'll
sneak in a 'subscribe' command between the 'affects' and the other
@@ -1642,7 +1641,7 @@
The user is a bug supervisors of the upstream product
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> bug_one = getUtility(IBugSet).get(1)
>>> submit_commands(
... bug_one, 'status confirmed', 'assignee test@xxxxxxxxxxxxx')
@@ -1746,6 +1745,7 @@
If none of the bug tasks can be chosen, an error message is sent to the
user, telling him that he has to use the 'affects' command.
+ >>> del stub.test_emails[:]
>>> login('stuart.bishop@xxxxxxxxxxxxx')
>>> submit_commands(
... bug_one, 'status new', 'assignee foo.bar@xxxxxxxxxxxxx')
@@ -1776,7 +1776,9 @@
him about the error. Let's start with trying to submit a bug without
signing the mail:
- >>> login('test@xxxxxxxxxxxxx')
+ >>> del stub.test_emails[:]
+ >>> login(sampledata.USER_EMAIL)
+ >>> simulate_receiving_untrusted_mail()
>>> from canonical.launchpad.mail import signed_message_from_string
>>> msg = signed_message_from_string(submit_mail)
@@ -1784,7 +1786,7 @@
>>> msg['Message-Id'] = email.Utils.make_msgid()
>>> handler.process(
... msg, msg['To'],
- ... extract_signature_timestamp=fake_extract_signature_timestamp)
+ ... )
True
>>> print_latest_email()
Subject: Submit Request Failure
@@ -1797,6 +1799,7 @@
A submit without specifying on what we want to file the bug on:
+ >>> login(sampledata.USER_EMAIL)
>>> submit_mail_no_bugtask = """From: test@xxxxxxxxxxxxx
... To: new@malone
... Date: Fri Jun 17 10:20:23 BST 2005
@@ -2003,7 +2006,7 @@
>>> from canonical.launchpad.mailnotification import (
... send_process_error_notification)
>>> send_process_error_notification(
- ... 'test@xxxxxxxxxxxxx', 'Some subject', 'Some error message.',
+ ... sampledata.USER_EMAIL, 'Some subject', 'Some error message.',
... msg, failing_command=['foo bar'])
The To and Subject headers got set to the values we provided:
@@ -2067,7 +2070,7 @@
First, we create a new firefox bug.
- >>> login('test@xxxxxxxxxxxxx')
+ >>> login(sampledata.USER_EMAIL)
>>> submit_mail = """From: Sample Person <test@xxxxxxxxxxxxx>
... To: new@xxxxxxxxxxxxxxxxxx
... Date: Fri Jun 17 10:20:23 BST 2006
=== modified file 'lib/lp/services/mail/incoming.py'
--- lib/lp/services/mail/incoming.py 2010-10-23 16:45:43 +0000
+++ lib/lp/services/mail/incoming.py 2010-11-25 04:47:03 +0000
@@ -37,6 +37,9 @@
from canonical.launchpad.interfaces.mailbox import IMailBox
from canonical.launchpad.mail.commands import get_error_message
from canonical.launchpad.mail.handlers import mail_handlers
+from canonical.launchpad.mail.helpers import (
+ ensure_sane_signature_timestamp,
+ )
from canonical.launchpad.mailnotification import (
send_process_error_notification,
)
@@ -61,6 +64,7 @@
# Match trailing whitespace.
trailing_whitespace = re.compile(r'[ \t]*((?=\r\n)|$)')
+
def canonicalise_line_endings(text):
r"""Canonicalise the line endings to '\r\n'.
@@ -159,14 +163,23 @@
return True
-def authenticateEmail(mail):
+def authenticateEmail(mail,
+ signature_timestamp_checker=None):
"""Authenticates an email by verifying the PGP signature.
The mail is expected to be an ISignedMessage.
+
+ If this completes, it will set the current security principal to be the
+ message sender.
+
+ :param signature_timestamp_checker: This callable is
+ passed the message signature timestamp, and it can raise an exception if
+ it dislikes it (for example as a replay attack.) This parameter is
+ intended for use in tests. If None, ensure_sane_signature_timestamp
+ is used.
"""
signature = mail.signature
- signed_content = mail.signedContent
name, email_addr = parseaddr(mail['From'])
authutil = getUtility(IPlacelessAuthUtility)
@@ -206,11 +219,19 @@
gpghandler = getUtility(IGPGHandler)
try:
sig = gpghandler.getVerifiedSignature(
- canonicalise_line_endings(signed_content), signature)
+ canonicalise_line_endings(mail.signedContent), signature)
except GPGVerificationError, e:
# verifySignature failed to verify the signature.
raise InvalidSignature("Signature couldn't be verified: %s" % str(e))
+ if signature_timestamp_checker is None:
+ signature_timestamp_checker = ensure_sane_signature_timestamp
+ # If this fails, we return an error to the user rather than just treating
+ # it as untrusted, so they can debug or understand the problem.
+ signature_timestamp_checker(
+ sig.timestamp,
+ 'incoming mail verification')
+
for gpgkey in person.gpg_keys:
if gpgkey.fingerprint == sig.fingerprint:
break
@@ -255,7 +276,8 @@
return request.oopsid
-def handleMail(trans=transaction):
+def handleMail(trans=transaction,
+ signature_timestamp_checker=None):
# First we define an error handler. We define it as a local
# function, to avoid having to pass a lot of parameters.
# pylint: disable-msg=W0631
@@ -358,7 +380,8 @@
continue
try:
- principal = authenticateEmail(mail)
+ principal = authenticateEmail(
+ mail, signature_timestamp_checker)
except InvalidSignature, error:
_handle_user_error(error, mail)
continue
=== modified file 'lib/lp/services/mail/tests/incomingmail.txt'
--- lib/lp/services/mail/tests/incomingmail.txt 2010-10-25 13:16:10 +0000
+++ lib/lp/services/mail/tests/incomingmail.txt 2010-11-25 04:47:03 +0000
@@ -62,6 +62,12 @@
... msg.replace_header('To', '123@%s' % domain)
... msgids[domain].append("<%s>" % sendmail(msg))
+handleMail will check the timestamp on signed messages, but the signatures
+on our testdata are old, and in these tests we don't care to be told.
+
+ >>> def accept_any_timestamp(timestamp, context_message):
+ ... pass
+
Since the User gets authenticated using OpenPGP signatures we have to
import the keys before handleMail is called.
@@ -72,6 +78,11 @@
>>> LaunchpadZopelessLayer.switchDbUser(config.processmail.dbuser)
>>> zopeless_transaction = LaunchpadZopelessLayer.txn
+ >>> handleMailForTest = lambda: handleMail(
+ ... zopeless_transaction,
+ ... signature_timestamp_checker=accept_any_timestamp)
+
+
We temporarily override the error mails' From address, so that they will
pass through the authentication stage:
@@ -87,7 +98,7 @@
discussed below; this output merely shows that we emit warnings when the
header is missing.)
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
WARNING:lp.services.mail:No X-Launchpad-Original-To header was present ...
WARNING:lp.services.mail:No X-Launchpad-Original-To header was present ...
WARNING:lp.services.mail:No X-Launchpad-Original-To header was present ...
@@ -139,7 +150,7 @@
>>> moin_change = read_test_message('moin-change.txt')
>>> moin_change['X-Launchpad-Original-To'] = '123@xxxxxxx'
>>> msgid = "<%s>" % sendmail(moin_change)
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid not in foo_handler.handledMails
True
@@ -154,7 +165,7 @@
>>> moin_change.replace_header('X-Launchpad-Original-To', '123@xxxxxxx')
>>> msgid = "<%s>" % sendmail(moin_change)
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid in bar_handler.handledMails
True
@@ -176,13 +187,13 @@
>>> msg = read_test_message('unsigned_inactive.txt')
>>> msgid = sendmail(msg, ['edit@malone-domain'])
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid not in foo_handler.handledMails
True
>>> msg = read_test_message('invalid_signed_inactive.txt')
>>> msgid = sendmail(msg, ['edit@malone-domain'])
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid not in foo_handler.handledMails
True
@@ -198,7 +209,7 @@
>>> msg['CC'] = '123@xxxxxxx'
>>> msg['X-Launchpad-Original-To'] = '123@xxxxxxx'
>>> msgid = '<%s>' % sendmail (msg, ['123@xxxxxxx'])
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid in bar_handler.handledMails
True
@@ -238,7 +249,7 @@
... doesn't matter
... """)
>>> msgid = sendmail(msg, ['edit@malone-domain'])
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
ERROR:canonical.launchpad.mail:An exception was raised inside the handler:
...
TestOopsException
@@ -286,7 +297,7 @@
... doesn't matter
... """)
>>> msgid = sendmail(msg, ['edit@malone-domain'])
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
ERROR:canonical.launchpad.mail:An exception was raised inside the handler:
...
Unauthorized
@@ -352,7 +363,7 @@
If we call handleMail(), we'll see some useful error messages printed
out:
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
ERROR:...:An exception was raised inside the handler: http://...
Traceback (most recent call last):
...
@@ -389,7 +400,7 @@
>>> len(stub.test_emails)
2
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
ERROR:...:Upload to Librarian failed...
...
UploadFailed: ...Connection refused...
@@ -416,7 +427,7 @@
>>> msg.replace_header('To', '123@xxxxxxx')
>>> msg['Return-Path'] = '<>'
>>> msgid = '<%s>' % sendmail(msg)
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid in foo_handler.handledMails
False
@@ -437,7 +448,7 @@
... 'multipart/report; report-type=delivery-status;'
... ' boundary="boundary"')
>>> msgid = '<%s>' % sendmail(msg)
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid in foo_handler.handledMails
False
@@ -452,7 +463,7 @@
>>> msg['Return-Path'] = '<not@xxxxxxxxx>'
>>> msg['Precedence'] = 'bulk'
>>> msgid = '<%s>' % sendmail(msg)
- >>> handleMail(zopeless_transaction)
+ >>> handleMailForTest()
>>> msgid in foo_handler.handledMails
False
=== modified file 'lib/lp/services/mail/tests/test_incoming.py'
--- lib/lp/services/mail/tests/test_incoming.py 2010-10-15 20:44:53 +0000
+++ lib/lp/services/mail/tests/test_incoming.py 2010-11-25 04:47:03 +0000
@@ -10,17 +10,23 @@
from zope.security.management import setSecurityPolicy
from canonical.config import config
+from canonical.launchpad.ftests import import_secret_test_key
from canonical.launchpad.mail.ftests.helpers import testmails_path
from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
from canonical.launchpad.webapp.authorization import LaunchpadSecurityPolicy
+from canonical.launchpad.mail import (
+ authenticateEmail,
+ helpers,
+ )
+from canonical.testing.layers import LaunchpadZopelessLayer
from lp.services.mail.incoming import (
handleMail,
MailErrorUtility,
)
-from canonical.testing.layers import LaunchpadZopelessLayer
from lp.services.mail.sendmail import MailController
from lp.services.mail.stub import TestMailer
from lp.testing import TestCaseWithFactory
+from lp.testing.factory import GPGSigningContext
from lp.testing.mail_helpers import pop_notifications
@@ -81,6 +87,23 @@
else:
self.assertEqual(old_oops.id, current_oops.id)
+ def test_bad_signature_timestamp(self):
+ """If the signature is nontrivial future-dated, it's not trusted."""
+
+ signing_context = GPGSigningContext(
+ import_secret_test_key().fingerprint, password='test')
+ msg = self.factory.makeSignedMessage(signing_context=signing_context)
+ # It's not trivial to make a gpg signature with a bogus timestamp, so
+ # let's just treat everything as invalid, and trust that the regular
+ # implementation of extraction and checking of timestamps is correct,
+ # or at least tested.
+ def fail_all_timestamps(timestamp, context):
+ raise helpers.IncomingEmailError("fail!")
+ self.assertRaises(
+ helpers.IncomingEmailError,
+ authenticateEmail,
+ msg, fail_all_timestamps)
+
def setUp(test):
test._old_policy = setSecurityPolicy(LaunchpadSecurityPolicy)
=== modified file 'utilities/migrater/file-ownership.txt'
--- utilities/migrater/file-ownership.txt 2010-11-16 12:56:01 +0000
+++ utilities/migrater/file-ownership.txt 2010-11-25 04:47:03 +0000
@@ -1047,7 +1047,7 @@
./mail/errortemplates/num-arguments-mismatch.txt
./mail/errortemplates/no-such-bug.txt
./mail/errortemplates/security-parameter-mismatch.txt
- ./mail/errortemplates/not-gpg-signed.txt
+ ./mail/errortemplates/unauthenticated-bug-creation.txt
./mail/errortemplates/key-not-registered.txt
./mail/errortemplates/oops.txt
./mail/errortemplates/branchmergeproposal-exists.txt
Follow ups