Merge lp:~julian-edwards/launchpad/poppy-warn-unsigned-bug-374019 into lp:launchpad

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 12470
Proposed branch: lp:~julian-edwards/launchpad/poppy-warn-unsigned-bug-374019
Merge into: lp:launchpad
Diff against target: 273 lines (+184/-2)
5 files modified
daemons/poppy-sftp.tac (+4/-0)
lib/lp/poppy/tests/test_poppy.py (+32/-0)
lib/lp/poppy/tests/test_twistedftp.py (+94/-0)
lib/lp/poppy/twistedftp.py (+53/-1)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~julian-edwards/launchpad/poppy-warn-unsigned-bug-374019
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+51144@code.launchpad.net

Commit message

[r=adeuring][bug=374019] Add support to the new twisted-based poppy ftp server to reject invalidly gpg-signed uploaded .changes files directly in the FTP session.

Description of the change

Add support to the new twisted-based poppy ftp server to reject invalidly gpg-signed uploaded .changes files directly in the FTP session.

This change depends on a patch to Twisted being applied, which is being tracked at http://twistedmatrix.com/trac/ticket/4909

It works by creating a new custom file-writer, which checks to see if the file is a .changes file, and does a quick lookup of the GPG key before ending the transfer session.

I've tested this approach using dput, and dput gets a lovely error message printed out like this:

Uploading to dogfood (via ftp to dogfood.launchpad.net):
  Uploading hello_2.5-1ubuntu1.dsc: done.
  Uploading hello_2.5-1ubuntu1.diff.gz: done.
  Uploading hello_2.5-1ubuntu1_source.changes: 1k/2k550 ('Changes file must be signed with a valid GPG signature: Verification failed 3 times: ["(7, 8, u\'Bad signature\')", "(7, 8, u\'Bad signature\')", "(7, 8, u\'Bad signature\')"] ',): Permission denied.

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

(15:48:46) adeuring: bigjools: PoppyAnonymousShell.openForWriting() has a "catch-all" except. I think we do not want to catch KeyboardInterrupt and SystemExit. And I think we should log an error for other exceptions.
(15:49:00) adeuring: otherwise, the branch looks good, I think
(15:50:07) bigjools: adeuring: that was cargo-culted from the parent class... I wasn't sure what to do about it TBH
(15:51:40) adeuring: bigjools: admittedly, I am not familiar with twisted, so I am not sure about KBInterrupt and SystemExit. But nevertheless I think we should log other exceptions.
(15:52:54) adeuring: "eating" exceptions simply scares me ;)
(15:53:12) bigjools: adeuring: yeah
(15:54:27) bigjools: adeuring: so I'll re-raise KeyboardInterrupt and SystemExit
(15:54:44) bigjools: adeuring: oh wait
(15:55:23) bigjools: adeuring: yes twisted has confused us both momentarily - the defer.fail() is effectively the same as re-raising the exception, it gets dealt with later
(15:56:10) adeuring: bigjools: intersting. So, IOError and OSError are re-raised too?
(15:56:19) adeuring: ah, no, there no fail() call
(15:56:26) bigjools: adeuring: no, they are converted to ftp errors
(15:56:48) bigjools: ftp.errnoToFailure will return a defer.fail
(15:57:23) adeuring: bigjools: right, thanks for the explanation, so r=me. But perhaps a small comment that twisted properly deals with the exceptions? (just to avoid the same question I asked again)?
(15:57:36) bigjools: adeuring: yup, will do, thanks for the review

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'daemons/poppy-sftp.tac'
2--- daemons/poppy-sftp.tac 2011-02-23 11:52:58 +0000
3+++ daemons/poppy-sftp.tac 2011-02-25 16:39:05 +0000
4@@ -19,6 +19,7 @@
5
6 from canonical.config import config
7 from canonical.launchpad.daemons import readyservice
8+from canonical.launchpad.scripts import execute_zcml_for_scripts
9
10 from lp.poppy import get_poppy_root
11 from lp.poppy.twistedftp import (
12@@ -105,5 +106,8 @@
13 banner=config.poppy.banner)
14 svc.setServiceParent(application)
15
16+# We need Zope for looking up the GPG utilities.
17+execute_zcml_for_scripts()
18+
19 # Service that announces when the daemon is ready
20 readyservice.ReadyService().setServiceParent(application)
21
22=== modified file 'lib/lp/poppy/tests/test_poppy.py'
23--- lib/lp/poppy/tests/test_poppy.py 2011-02-24 17:23:57 +0000
24+++ lib/lp/poppy/tests/test_poppy.py 2011-02-25 16:39:05 +0000
25@@ -5,6 +5,7 @@
26
27 __metaclass__ = type
28
29+import ftplib
30 import os
31 import shutil
32 import stat
33@@ -37,6 +38,7 @@
34 )
35 from lp.poppy.hooks import Hooks
36 from lp.testing import TestCaseWithFactory
37+from lp.testing.keyserver import KeyServerTac
38
39
40 class FTPServer(Fixture):
41@@ -371,6 +373,36 @@
42 self.root_dir, upload_dirs[index], "test")).read()
43 self.assertEqual(content, expected_contents[index])
44
45+ def test_bad_gpg_on_changesfile(self):
46+ """Check that we get a rejection error when uploading .changes files
47+ with invalid GPG signatures.
48+ """
49+ # Start up the poppy server.
50+ self.server.waitForStartUp()
51+
52+ transport = self.server.getTransport()
53+ if transport.external_url().startswith("sftp"):
54+ # We're not GPG-checking sftp uploads right now.
55+ return
56+
57+ # Start up the test keyserver.
58+ tac = KeyServerTac()
59+ tac.setUp()
60+ self.addCleanup(tac.tearDown)
61+
62+ fake_file = StringIO.StringIO("fake contents")
63+
64+ # We can't use bzrlib's transport here because it uploads a
65+ # renamed file before renaming it on the server.
66+ f = ftplib.FTP()
67+ f.connect(host="localhost", port=config.poppy.ftp_port)
68+ f.login()
69+ self.assertRaises(
70+ ftplib.error_perm,
71+ f.storbinary,
72+ 'STOR '+'foo_source.changes',
73+ fake_file)
74+
75
76 def test_suite():
77 tests = unittest.TestLoader().loadTestsFromName(__name__)
78
79=== added file 'lib/lp/poppy/tests/test_twistedftp.py'
80--- lib/lp/poppy/tests/test_twistedftp.py 1970-01-01 00:00:00 +0000
81+++ lib/lp/poppy/tests/test_twistedftp.py 2011-02-25 16:39:05 +0000
82@@ -0,0 +1,94 @@
83+# Copyright 2011 Canonical Ltd. This software is licensed under the
84+# GNU Affero General Public License version 3 (see the file LICENSE).
85+
86+"""Tests for Twisted Poppy FTP."""
87+
88+__metaclass__ = type
89+
90+import os
91+
92+from testtools.deferredruntest import (
93+ AsynchronousDeferredRunTest,
94+ )
95+from twisted.protocols import ftp
96+from zope.component import getUtility
97+
98+from canonical.config import config
99+from canonical.launchpad.ftests.keys_for_tests import gpgkeysdir
100+from canonical.launchpad.interfaces.gpghandler import IGPGHandler
101+from canonical.testing.layers import ZopelessDatabaseLayer
102+
103+from lp.poppy.twistedftp import PoppyFileWriter
104+from lp.registry.interfaces.gpg import (
105+ GPGKeyAlgorithm,
106+ IGPGKeySet)
107+from lp.testing import TestCaseWithFactory
108+from lp.testing.keyserver import KeyServerTac
109+
110+
111+class TestPoppyFileWriter(TestCaseWithFactory):
112+
113+ layer = ZopelessDatabaseLayer
114+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20)
115+
116+ def setUp(self):
117+ TestCaseWithFactory.setUp(self)
118+
119+ # Start the test keyserver. Starting up and tearing this down
120+ # for each test is expensive, but we don't have a way of having
121+ # cross-test persistent fixtures yet. See bug 724349.
122+ self.tac = KeyServerTac()
123+ self.tac.setUp()
124+ self.addCleanup(self.tac.tearDown)
125+
126+ # Load a key.
127+ gpg_handler = getUtility(IGPGHandler)
128+ key_path = os.path.join(gpgkeysdir, 'ftpmaster@canonical.com.pub')
129+ key_data = open(key_path).read()
130+ key = gpg_handler.importPublicKey(key_data)
131+ assert key is not None
132+
133+ # Make a new user and add the above key to it.
134+ user = self.factory.makePerson()
135+ key_set = getUtility(IGPGKeySet)
136+ user_key = key_set.new(
137+ ownerID=user.id, keyid=key.keyid, fingerprint=key.fingerprint,
138+ algorithm=GPGKeyAlgorithm.items[key.algorithm],
139+ keysize=key.keysize, can_encrypt=key.can_encrypt,
140+ active=True)
141+
142+ # Locate the directory with test files.
143+ self.test_files_dir = os.path.join(
144+ config.root, "lib/lp/soyuz/scripts/tests/upload_test_files/")
145+
146+ def test_changes_file_with_valid_GPG(self):
147+ valid_changes_file = os.path.join(
148+ self.test_files_dir, "etherwake_1.08-1_source.changes")
149+
150+ def callback(result):
151+ self.assertIs(None, result)
152+
153+ with open(valid_changes_file) as opened_file:
154+ file_writer = PoppyFileWriter(opened_file)
155+ d = file_writer.close()
156+ d.addBoth(callback)
157+ return d
158+
159+ def test_changes_file_with_invalid_GPG(self):
160+ invalid_changes_file = os.path.join(
161+ self.test_files_dir, "broken_source.changes")
162+
163+ def error_callback(failure):
164+ self.assertTrue(failure.check, ftp.PermissionDeniedError)
165+ self.assertIn(
166+ "Changes file must be signed with a valid GPG signature",
167+ failure.getErrorMessage())
168+
169+ def success_callback(result):
170+ self.fail("Success when there should have been failure.")
171+
172+ with open(invalid_changes_file) as opened_file:
173+ file_writer = PoppyFileWriter(opened_file)
174+ d = file_writer.close()
175+ d.addCallbacks(success_callback, error_callback)
176+ return d
177
178=== modified file 'lib/lp/poppy/twistedftp.py'
179--- lib/lp/poppy/twistedftp.py 2011-02-23 14:24:43 +0000
180+++ lib/lp/poppy/twistedftp.py 2011-02-25 16:39:05 +0000
181@@ -21,11 +21,18 @@
182 from twisted.python import filepath
183
184 from zope.interface import implements
185+from zope.component import getUtility
186+
187+from canonical.launchpad.interfaces.gpghandler import (
188+ GPGVerificationError,
189+ IGPGHandler,
190+ )
191
192 from canonical.config import config
193 from lp.poppy import get_poppy_root
194 from lp.poppy.filesystem import UploadFileSystem
195 from lp.poppy.hooks import Hooks
196+from lp.registry.interfaces.gpg import IGPGKeySet
197
198
199 class PoppyAccessCheck:
200@@ -72,7 +79,15 @@
201 """
202 filename = os.sep.join(file_segments)
203 self._create_missing_directories(filename)
204- return super(PoppyAnonymousShell, self).openForWriting(file_segments)
205+ path = self._path(file_segments)
206+ try:
207+ fObj = path.open("w")
208+ except (IOError, OSError), e:
209+ return ftp.errnoToFailure(e.errno, path)
210+ except:
211+ # Push any other error up to Twisted to deal with.
212+ return defer.fail()
213+ return defer.succeed(PoppyFileWriter(fObj))
214
215 def makeDirectory(self, path):
216 """Make a directory using the secure `UploadFileSystem`."""
217@@ -128,6 +143,43 @@
218 "Only IFTPShell interface is supported by this realm")
219
220
221+class PoppyFileWriter(ftp._FileWriter):
222+ """An `IWriteFile` that checks for signed changes files."""
223+
224+ def close(self):
225+ """Called after the file has been completely downloaded."""
226+ if self.fObj.name.endswith(".changes"):
227+ error = self.validateGPG(self.fObj.name)
228+ if error is not None:
229+ # PermissionDeniedError is one of the few ftp exceptions
230+ # that lets us pass an error string back to the client.
231+ return defer.fail(ftp.PermissionDeniedError(error))
232+ return defer.succeed(None)
233+
234+ def validateGPG(self, signed_file):
235+ """Check the GPG signature in the file referenced by signed_file.
236+
237+ Return an error string if there's a problem, or None.
238+ """
239+ try:
240+ sig = getUtility(IGPGHandler).getVerifiedSignatureResilient(
241+ file(signed_file, "rb").read())
242+ except GPGVerificationError, error:
243+ return ("Changes file must be signed with a valid GPG "
244+ "signature: %s" % error)
245+
246+ key = getUtility(IGPGKeySet).getByFingerprint(sig.fingerprint)
247+ if key is None:
248+ return (
249+ "Signing key %s not registered in launchpad."
250+ % sig.fingerprint)
251+
252+ if key.active == False:
253+ return "Changes file is signed with a deactivated key"
254+
255+ return None
256+
257+
258 class FTPServiceFactory(service.Service):
259 """A factory that makes an `FTPService`"""
260
261
262=== modified file 'versions.cfg'
263--- versions.cfg 2011-02-23 16:14:05 +0000
264+++ versions.cfg 2011-02-25 16:39:05 +0000
265@@ -75,7 +75,7 @@
266 storm = 0.18
267 testtools = 0.9.8
268 transaction = 1.0.0
269-Twisted = 10.2.0-4395fix-1
270+Twisted = 10.2.0-4395fix-1-4909
271 uuid = 1.30
272 van.testing = 2.0.1
273 wadllib = 1.2.0