Merge lp:~bzr/bzr/urllib-verifies-ssl-certs into lp:bzr/2.5

Proposed by Vincent Ladeuil on 2012-01-20
Status: Merged
Approved by: Vincent Ladeuil on 2012-01-20
Approved revision: 6265
Merged at revision: 6455
Proposed branch: lp:~bzr/bzr/urllib-verifies-ssl-certs
Merge into: lp:bzr/2.5
Prerequisite: lp:~jelmer/bzr/urllib-verifies-ssl-certs
Diff against target: 476 lines (+112/-79)
9 files modified
bzrlib/plugins/launchpad/lp_registration.py (+1/-5)
bzrlib/plugins/launchpad/test_lp_directory.py (+9/-4)
bzrlib/tests/https_server.py (+7/-1)
bzrlib/tests/test_http.py (+21/-15)
bzrlib/tests/test_https_urllib.py (+28/-28)
bzrlib/transport/__init__.py (+3/-3)
bzrlib/transport/http/_urllib.py (+15/-4)
bzrlib/transport/http/_urllib2_wrappers.py (+24/-13)
doc/en/release-notes/bzr-2.5.txt (+4/-6)
To merge this branch: bzr merge lp:~bzr/bzr/urllib-verifies-ssl-certs
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) 2012-01-20 Approve on 2012-01-20
Review via email: mp+89437@code.launchpad.net

Commit Message

Verify ssl certs for the http urllib implementation.

Description of the Change

THis builds on top of lp:~jelmer/bzr/urllib-verifies-ssl-certs to fix the
tests failures.

There were two kinds of hangs:

- from the client because the ssl errors weren't properly interpreted as
  *not* transient (and were retried against a server which had no idea what
  the client would want ;)

- from the server (a bit surprising since such hangs shouldn't occur anymore
  ?) because if the ssl handshake fails the request shouldn't be processed
  (but the exception needed to be caught to recognize this).

In the future, we probably want to make ssl.ca_certs and ssl.cert_reqs
authentication config options so that proxies can use different options but
that in itself is also quite invasive and out of scope here. These are my
excuses for the semi-hackish way the ca_certs are propagated from the
transport to the _urllib2_wrappers classes. In the end, the plan is to make
them part of auth and proxy_auth credentials which will be a bit less
invasive and open to other options (self signed certificates for example).

Despite its docstring, match_hostname accept our test server 127.0.0.1 IP
address without a wink, I didn't investigate whether it's by luck or by
design. Any ideas ?

Anyway, this seems the fastest way to get certificate verification landed
for 2.5.

To post a comment you must log in.
Jelmer Vernooij (jelmer) wrote :

line 69:
+ # FIXME: We proabaly want more tests to capture which ssl
probably?

I also think this is fine for 2.5, so let's JFLI.

review: Approve
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/plugins/launchpad/lp_registration.py'
2--- bzrlib/plugins/launchpad/lp_registration.py 2011-12-19 10:58:39 +0000
3+++ bzrlib/plugins/launchpad/lp_registration.py 2012-01-20 13:52:48 +0000
4@@ -56,11 +56,7 @@
5 class XMLRPCTransport(xmlrpclib.Transport):
6
7 def __init__(self, scheme):
8- # In python2.4 xmlrpclib.Transport is a old-style class, and does not
9- # define __init__, so we check first
10- init = getattr(xmlrpclib.Transport, '__init__', None)
11- if init is not None:
12- init(self)
13+ xmlrpclib.Transport.__init__(self)
14 self._scheme = scheme
15 self._opener = _urllib2_wrappers.Opener()
16 self.verbose = 0
17
18=== modified file 'bzrlib/plugins/launchpad/test_lp_directory.py'
19--- bzrlib/plugins/launchpad/test_lp_directory.py 2011-10-06 06:45:46 +0000
20+++ bzrlib/plugins/launchpad/test_lp_directory.py 2012-01-20 13:52:48 +0000
21@@ -19,6 +19,7 @@
22 import os
23 import xmlrpclib
24
25+import bzrlib
26 from bzrlib import (
27 debug,
28 errors,
29@@ -29,6 +30,7 @@
30 from bzrlib.directory_service import directories
31 from bzrlib.tests import (
32 features,
33+ ssl_certs,
34 TestCaseInTempDir,
35 TestCaseWithMemoryTransport
36 )
37@@ -452,12 +454,15 @@
38 tests.TestCase.setUp(self)
39 self.server = self.server_class()
40 self.server.start_server()
41+ self.addCleanup(self.server.stop_server)
42 # Ensure we don't clobber env
43 self.overrideEnv('BZR_LP_XMLRPC_URL', None)
44-
45- def tearDown(self):
46- self.server.stop_server()
47- tests.TestCase.tearDown(self)
48+ # Ensure we use the right certificates for https.
49+ # FIXME: There should be a better way but the only alternative I can
50+ # think of involves carrying the ca_certs through the lp_registration
51+ # infrastructure to _urllib2_wrappers... -- vila 2012-01-20
52+ bzrlib.global_state.cmdline_overrides._from_cmdline(
53+ ['ssl.ca_certs=%s' % ssl_certs.build_path('ca.crt')])
54
55 def set_canned_response(self, server, path):
56 response_format = '''HTTP/1.1 200 OK\r
57
58=== modified file 'bzrlib/tests/https_server.py'
59--- bzrlib/tests/https_server.py 2011-01-10 22:20:12 +0000
60+++ bzrlib/tests/https_server.py 2012-01-20 13:52:48 +0000
61@@ -49,7 +49,13 @@
62 serving = test_server.TestingTCPServerMixin.verify_request(
63 self, request, client_address)
64 if serving:
65- request.do_handshake()
66+ try:
67+ request.do_handshake()
68+ except ssl.SSLError, e:
69+ # FIXME: We proabaly want more tests to capture which ssl
70+ # errors are worth reporting but mostly our tests want an https
71+ # server that works -- vila 2012-01-19
72+ return False
73 return serving
74
75 def ignored_exceptions_during_shutdown(self, e):
76
77=== modified file 'bzrlib/tests/test_http.py'
78--- bzrlib/tests/test_http.py 2012-01-20 13:52:46 +0000
79+++ bzrlib/tests/test_http.py 2012-01-20 13:52:48 +0000
80@@ -127,22 +127,29 @@
81 ('urllib,http', dict(_activity_server=ActivityHTTPServer,
82 _transport=_urllib.HttpTransport_urllib,)),
83 ]
84+ if features.pycurl.available():
85+ activity_scenarios.append(
86+ ('pycurl,http', dict(_activity_server=ActivityHTTPServer,
87+ _transport=PyCurlTransport,)),)
88 if features.HTTPSServerFeature.available():
89+ # FIXME: Until we have a better way to handle self-signed certificates
90+ # (like allowing them in a test specific authentication.conf for
91+ # example), we need some specialized pycurl/urllib transport for tests.
92+ # -- vila 2012-01-20
93+ from bzrlib.tests import (
94+ ssl_certs,
95+ )
96+ class HTTPS_urllib_transport(_urllib.HttpTransport_urllib):
97+
98+ def __init__(self, base, _from_transport=None):
99+ super(HTTPS_urllib_transport, self).__init__(
100+ base, _from_transport=_from_transport,
101+ ca_certs=ssl_certs.build_path('ca.crt'))
102+
103 activity_scenarios.append(
104 ('urllib,https', dict(_activity_server=ActivityHTTPSServer,
105- _transport=_urllib.HttpTransport_urllib,)),)
106- if features.pycurl.available():
107- activity_scenarios.append(
108- ('pycurl,http', dict(_activity_server=ActivityHTTPServer,
109- _transport=PyCurlTransport,)),)
110- if features.HTTPSServerFeature.available():
111- from bzrlib.tests import (
112- ssl_certs,
113- )
114- # FIXME: Until we have a better way to handle self-signed
115- # certificates (like allowing them in a test specific
116- # authentication.conf for example), we need some specialized pycurl
117- # transport for tests.
118+ _transport=HTTPS_urllib_transport,)),)
119+ if features.pycurl.available():
120 class HTTPS_pycurl_transport(PyCurlTransport):
121
122 def __init__(self, base, _from_transport=None):
123@@ -2119,18 +2126,17 @@
124 tests.TestCase.setUp(self)
125 self.server = self._activity_server(self._protocol_version)
126 self.server.start_server()
127+ self.addCleanup(self.server.stop_server)
128 _activities = {} # Don't close over self and create a cycle
129 def report_activity(t, bytes, direction):
130 count = _activities.get(direction, 0)
131 count += bytes
132 _activities[direction] = count
133 self.activities = _activities
134-
135 # We override at class level because constructors may propagate the
136 # bound method and render instance overriding ineffective (an
137 # alternative would be to define a specific ui factory instead...)
138 self.overrideAttr(self._transport, '_report_activity', report_activity)
139- self.addCleanup(self.server.stop_server)
140
141 def get_transport(self):
142 t = self._transport(self.server.get_url())
143
144=== modified file 'bzrlib/tests/test_https_urllib.py'
145--- bzrlib/tests/test_https_urllib.py 2012-01-20 13:52:46 +0000
146+++ bzrlib/tests/test_https_urllib.py 2012-01-20 13:52:48 +0000
147@@ -18,18 +18,17 @@
148
149 """
150
151+import os
152 import ssl
153
154-from bzrlib import trace
155+from bzrlib import (
156+ config,
157+ trace,
158+ )
159 from bzrlib.errors import (
160 CertificateError,
161 ConfigOptionValueError,
162 )
163-from bzrlib.config import (
164- IniFileStore,
165- Stack,
166- )
167-import os
168 from bzrlib.tests import (
169 TestCase,
170 TestCaseInTempDir,
171@@ -37,23 +36,20 @@
172 from bzrlib.transport.http import _urllib2_wrappers
173
174
175-def stack_from_string(text):
176- store = IniFileStore()
177- store._load_from_string(text)
178- return Stack([store.get_sections])
179-
180-
181 class CaCertsConfigTests(TestCaseInTempDir):
182
183+ def get_stack(self, content):
184+ return config.MemoryStack(content.encode('utf-8'))
185+
186 def test_default_raises_value_error(self):
187- stack = stack_from_string("")
188+ stack = self.get_stack("")
189 self.overrideAttr(_urllib2_wrappers, "DEFAULT_CA_PATH",
190 "/i-do-not-exist")
191 self.assertRaises(ValueError, stack.get, 'ssl.ca_certs')
192
193 def test_default_exists(self):
194 self.build_tree(['cacerts.pem'])
195- stack = stack_from_string("")
196+ stack = self.get_stack("")
197 path = os.path.join(self.test_dir, "cacerts.pem")
198 self.overrideAttr(_urllib2_wrappers, "DEFAULT_CA_PATH", path)
199 self.assertEquals(path, stack.get('ssl.ca_certs'))
200@@ -61,49 +57,53 @@
201 def test_specified(self):
202 self.build_tree(['cacerts.pem'])
203 path = os.path.join(self.test_dir, "cacerts.pem")
204- stack = stack_from_string("ssl.ca_certs = %s\n" % path)
205+ stack = self.get_stack("ssl.ca_certs = %s\n" % path)
206 self.assertEquals(path, stack.get('ssl.ca_certs'))
207
208 def test_specified_doesnt_exist(self):
209 path = os.path.join(self.test_dir, "nonexisting.pem")
210- stack = stack_from_string("ssl.ca_certs = %s\n" % path)
211+ stack = self.get_stack("ssl.ca_certs = %s\n" % path)
212 self.warnings = []
213 def warning(*args):
214 self.warnings.append(args[0] % args[1:])
215 self.overrideAttr(trace, 'warning', warning)
216- self.assertEquals(_urllib2_wrappers.DEFAULT_CA_PATH, stack.get('ssl.ca_certs'))
217+ self.assertEquals(_urllib2_wrappers.DEFAULT_CA_PATH,
218+ stack.get('ssl.ca_certs'))
219 self.assertLength(1, self.warnings)
220- self.assertContainsRe(self.warnings[0], "is not valid for \"ssl.ca_certs\"")
221+ self.assertContainsRe(self.warnings[0],
222+ "is not valid for \"ssl.ca_certs\"")
223
224
225 class CertReqsConfigTests(TestCaseInTempDir):
226
227 def test_default(self):
228- stack = stack_from_string("")
229+ stack = config.MemoryStack("")
230 self.assertEquals(ssl.CERT_REQUIRED, stack.get("ssl.cert_reqs"))
231
232 def test_from_string(self):
233- stack = stack_from_string("ssl.cert_reqs = none\n")
234+ stack = config.MemoryStack("ssl.cert_reqs = none\n")
235 self.assertEquals(ssl.CERT_NONE, stack.get("ssl.cert_reqs"))
236- stack = stack_from_string("ssl.cert_reqs = optional\n")
237+ stack = config.MemoryStack("ssl.cert_reqs = optional\n")
238 self.assertEquals(ssl.CERT_OPTIONAL, stack.get("ssl.cert_reqs"))
239- stack = stack_from_string("ssl.cert_reqs = required\n")
240+ stack = config.MemoryStack("ssl.cert_reqs = required\n")
241 self.assertEquals(ssl.CERT_REQUIRED, stack.get("ssl.cert_reqs"))
242- stack = stack_from_string("ssl.cert_reqs = invalid\n")
243+ stack = config.MemoryStack("ssl.cert_reqs = invalid\n")
244 self.assertRaises(ConfigOptionValueError, stack.get, "ssl.cert_reqs")
245
246
247 class MatchHostnameTests(TestCase):
248
249 def test_no_certificate(self):
250- self.assertRaises(ValueError, _urllib2_wrappers.match_hostname({}))
251+ self.assertRaises(ValueError,
252+ _urllib2_wrappers.match_hostname, {}, "example.com")
253
254 def test_no_valid_attributes(self):
255- self.assertRaises(CertificateError,
256- _urllib2_wrappers.match_hostname({"Problem": "Solved"}))
257+ self.assertRaises(CertificateError, _urllib2_wrappers.match_hostname,
258+ {"Problem": "Solved"}, "example.com")
259
260 def test_common_name(self):
261 cert = {'subject': ((('commonName', 'example.com'),),)}
262- self.assertIs(None, _urllib2_wrappers.match_hostname(cert, "example.com"))
263+ self.assertIs(None,
264+ _urllib2_wrappers.match_hostname(cert, "example.com"))
265 self.assertRaises(CertificateError, _urllib2_wrappers.match_hostname,
266- cert, "example.org")
267+ cert, "example.org")
268
269=== modified file 'bzrlib/transport/__init__.py'
270--- bzrlib/transport/__init__.py 2012-01-20 13:52:46 +0000
271+++ bzrlib/transport/__init__.py 2012-01-20 13:52:48 +0000
272@@ -1782,15 +1782,15 @@
273 help="Read-only access of branches exported on the web.")
274 register_transport_proto('https://',
275 help="Read-only access of branches exported on the web using SSL.")
276-# The default http implementation is urllib
277+# The default http implementation is urllib, but https uses pycurl if available
278 register_lazy_transport('http://', 'bzrlib.transport.http._pycurl',
279 'PyCurlTransport')
280-register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
281- 'PyCurlTransport')
282 register_lazy_transport('http://', 'bzrlib.transport.http._urllib',
283 'HttpTransport_urllib')
284 register_lazy_transport('https://', 'bzrlib.transport.http._urllib',
285 'HttpTransport_urllib')
286+register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
287+ 'PyCurlTransport')
288
289 register_transport_proto('ftp://', help="Access using passive FTP.")
290 register_lazy_transport('ftp://', 'bzrlib.transport.ftp', 'FtpTransport')
291
292=== modified file 'bzrlib/transport/http/_urllib.py'
293--- bzrlib/transport/http/_urllib.py 2011-12-18 15:28:38 +0000
294+++ bzrlib/transport/http/_urllib.py 2012-01-20 13:52:48 +0000
295@@ -38,14 +38,14 @@
296
297 _opener_class = Opener
298
299- def __init__(self, base, _from_transport=None):
300+ def __init__(self, base, _from_transport=None, ca_certs=None):
301 super(HttpTransport_urllib, self).__init__(
302 base, 'urllib', _from_transport=_from_transport)
303 if _from_transport is not None:
304 self._opener = _from_transport._opener
305 else:
306 self._opener = self._opener_class(
307- report_activity=self._report_activity)
308+ report_activity=self._report_activity, ca_certs=ca_certs)
309
310 def _perform(self, request):
311 """Send the request to the server and handles common errors.
312@@ -175,7 +175,18 @@
313 )
314 permutations = [(HttpTransport_urllib, http_server.HttpServer_urllib),]
315 if features.HTTPSServerFeature.available():
316- from bzrlib.tests import https_server
317- permutations.append((HttpTransport_urllib,
318+ from bzrlib.tests import (
319+ https_server,
320+ ssl_certs,
321+ )
322+
323+ class HTTPS_urllib_transport(HttpTransport_urllib):
324+
325+ def __init__(self, base, _from_transport=None):
326+ super(HTTPS_urllib_transport, self).__init__(
327+ base, _from_transport=_from_transport,
328+ ca_certs=ssl_certs.build_path('ca.crt'))
329+
330+ permutations.append((HTTPS_urllib_transport,
331 https_server.HTTPSServer_urllib))
332 return permutations
333
334=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
335--- bzrlib/transport/http/_urllib2_wrappers.py 2012-01-20 13:52:46 +0000
336+++ bzrlib/transport/http/_urllib2_wrappers.py 2012-01-20 13:52:48 +0000
337@@ -122,7 +122,7 @@
338 Whether to require a certificate from the remote side. (default:required)
339
340 Possible values:
341- * none: certificates ignored
342+ * none: Certificates ignored
343 * optional: Certificates not required, but validated if provided
344 * required: Certificates required, and validated
345 """)
346@@ -356,11 +356,12 @@
347
348 # XXX: Needs refactoring at the caller level.
349 def __init__(self, host, port=None, proxied_host=None,
350- report_activity=None):
351+ report_activity=None, ca_certs=None):
352 AbstractHTTPConnection.__init__(self, report_activity=report_activity)
353 # Use strict=True since we don't support HTTP/0.9
354 httplib.HTTPConnection.__init__(self, host, port, strict=True)
355 self.proxied_host = proxied_host
356+ # ca_certs is ignored, it's only relevant for https
357
358 def connect(self):
359 if 'http' in debug.debug_flags:
360@@ -413,13 +414,12 @@
361 return
362 dnsnames.append(value)
363 if len(dnsnames) > 1:
364- raise errors.CertificateError("hostname %r "
365- "doesn't match either of %s"
366+ raise errors.CertificateError(
367+ "hostname %r doesn't match either of %s"
368 % (hostname, ', '.join(map(repr, dnsnames))))
369 elif len(dnsnames) == 1:
370- raise errors.CertificateError("hostname %r "
371- "doesn't match %r"
372- % (hostname, dnsnames[0]))
373+ raise errors.CertificateError("hostname %r doesn't match %r" %
374+ (hostname, dnsnames[0]))
375 else:
376 raise errors.CertificateError("no appropriate commonName or "
377 "subjectAltName fields were found")
378@@ -429,12 +429,13 @@
379
380 def __init__(self, host, port=None, key_file=None, cert_file=None,
381 proxied_host=None,
382- report_activity=None):
383+ report_activity=None, ca_certs=None):
384 AbstractHTTPConnection.__init__(self, report_activity=report_activity)
385 # Use strict=True since we don't support HTTP/0.9
386 httplib.HTTPSConnection.__init__(self, host, port,
387 key_file, cert_file, strict=True)
388 self.proxied_host = proxied_host
389+ self.ca_certs = ca_certs
390
391 def connect(self):
392 if 'http' in debug.debug_flags:
393@@ -447,7 +448,10 @@
394 def connect_to_origin(self):
395 # FIXME JRV 2011-12-18: Use location config here?
396 config_stack = config.GlobalStack()
397- ca_certs = config_stack.get('ssl.ca_certs')
398+ if self.ca_certs is None:
399+ ca_certs = config_stack.get('ssl.ca_certs')
400+ else:
401+ ca_certs = self.ca_certs
402 cert_reqs = config_stack.get('ssl.cert_reqs')
403 if cert_reqs == ssl.CERT_NONE:
404 trace.warning("not checking SSL certificates for %s: %d",
405@@ -581,8 +585,9 @@
406
407 handler_order = 1000 # after all pre-processings
408
409- def __init__(self, report_activity=None):
410+ def __init__(self, report_activity=None, ca_certs=None):
411 self._report_activity = report_activity
412+ self.ca_certs = ca_certs
413
414 def create_connection(self, request, http_connection_class):
415 host = request.get_host()
416@@ -596,7 +601,8 @@
417 try:
418 connection = http_connection_class(
419 host, proxied_host=request.proxied_host,
420- report_activity=self._report_activity)
421+ report_activity=self._report_activity,
422+ ca_certs=self.ca_certs)
423 except httplib.InvalidURL, exception:
424 # There is only one occurrence of InvalidURL in httplib
425 raise errors.InvalidURL(request.get_full_url(),
426@@ -788,6 +794,10 @@
427 % (request, request.connection.sock.getsockname())
428 response = connection.getresponse()
429 convert_to_addinfourl = True
430+ except (ssl.SSLError, errors.CertificateError):
431+ # Something is wrong with either the certificate or the hostname,
432+ # re-trying won't help
433+ raise
434 except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
435 socket.error, httplib.HTTPException):
436 response = self.retry_or_raise(http_class, request, first_try)
437@@ -1785,9 +1795,10 @@
438 connection=ConnectionHandler,
439 redirect=HTTPRedirectHandler,
440 error=HTTPErrorProcessor,
441- report_activity=None):
442+ report_activity=None,
443+ ca_certs=None):
444 self._opener = urllib2.build_opener(
445- connection(report_activity=report_activity),
446+ connection(report_activity=report_activity, ca_certs=ca_certs),
447 redirect, error,
448 ProxyHandler(),
449 HTTPBasicAuthHandler(),
450
451=== modified file 'doc/en/release-notes/bzr-2.5.txt'
452--- doc/en/release-notes/bzr-2.5.txt 2012-01-20 13:52:46 +0000
453+++ doc/en/release-notes/bzr-2.5.txt 2012-01-20 13:52:48 +0000
454@@ -53,6 +53,10 @@
455 * Test for equality instead of object identity where ROOT_PARENT is concerned.
456 (Wouter van Heyst, #881142)
457
458+* urllib-based HTTPS client connections now verify the server certificate
459+ validity as well as the hostname.
460+ (Jelmer Vernooij, Vincent Ladeuil, #651161)
461+
462 Documentation
463 *************
464
465@@ -540,12 +544,6 @@
466 to 'line-based'. This replace the previous shelf UI only patch using
467 ``INSIDE_EMACS``. (BenoƮt Pierre)
468
469-* urllib-based HTTPS client connections now verify the server certificate
470- as well as the hostname. (Jelmer Vernooij)
471-
472-* urllib-based HTTPS connections are now the default.
473- (Jelmer Vernooij, #125055, #651161)
474-
475 Bug Fixes
476 *********
477

Subscribers

People subscribed via source and target branches