Rev 6455: (vila) Verify ssl certs for the http urllib implementation. (Bazaar in file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/2.5/

Patch Queue Manager pqm at pqm.ubuntu.com
Fri Jan 20 16:42:28 UTC 2012


At file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/2.5/

------------------------------------------------------------
revno: 6455 [merge]
revision-id: pqm at pqm.ubuntu.com-20120120164227-ebgrwo6xobbvqhdf
parent: pqm at pqm.ubuntu.com-20120120161534-w0oirmaxdooo12c0
parent: v.ladeuil+lp at free.fr-20120120132423-cqamauv0s5fk11wl
committer: Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: 2.5
timestamp: Fri 2012-01-20 16:42:27 +0000
message:
  (vila) Verify ssl certs for the http urllib implementation. (Bazaar
   Developers)
added:
  bzrlib/tests/test_https_urllib.py test_https_urllib.py-20111220105828-v3g3fknv8inj2jqv-1
modified:
  bzrlib/config.py               config.py-20051011043216-070c74f4e9e338e8
  bzrlib/errors.py               errors.py-20050309040759-20512168c4e14fbd
  bzrlib/plugins/launchpad/lp_registration.py lp_registration.py-20060315190948-daa617eafe3a8d48
  bzrlib/plugins/launchpad/test_lp_directory.py test_lp_indirect.py-20070126002743-oyle362tzv9cd8mi-1
  bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
  bzrlib/tests/https_server.py   https_server.py-20071121173708-aj8zczi0ziwbwz21-1
  bzrlib/tests/test_http.py      testhttp.py-20051018020158-b2eef6e867c514d9
  bzrlib/transport/__init__.py   transport.py-20050711165921-4978aa7ce1285ad5
  bzrlib/transport/http/_urllib.py _urlgrabber.py-20060113083826-0bbf7d992fbf090c
  bzrlib/transport/http/_urllib2_wrappers.py _urllib2_wrappers.py-20060913231729-ha9ugi48ktx481ao-1
  doc/en/release-notes/bzr-2.5.txt bzr2.5.txt-20110708125756-587p0hpw7oke4h05-1
  doc/en/whats-new/whats-new-in-2.5.txt whatsnewin2.5.txt-20110711065040-xz9b4xba1qzlwp7m-1
=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py	2012-01-13 15:05:42 +0000
+++ b/bzrlib/config.py	2012-01-20 13:21:01 +0000
@@ -2866,6 +2866,13 @@
     Option('submit_to',
            help='''Where submissions from this branch are mailed to.'''))
 
+option_registry.register_lazy('ssl.ca_certs',
+    'bzrlib.transport.http._urllib2_wrappers', 'opt_ssl_ca_certs')
+
+option_registry.register_lazy('ssl.cert_reqs',
+    'bzrlib.transport.http._urllib2_wrappers', 'opt_ssl_cert_reqs')
+
+
 
 class Section(object):
     """A section defines a dict of option name => value.

=== modified file 'bzrlib/errors.py'
--- a/bzrlib/errors.py	2012-01-18 20:27:41 +0000
+++ b/bzrlib/errors.py	2012-01-20 13:21:01 +0000
@@ -1672,6 +1672,14 @@
         TransportError.__init__(self, msg, orig_error=orig_error)
 
 
+class CertificateError(TransportError):
+
+    _fmt = "Certificate error: %(error)s"
+
+    def __init__(self, error):
+        self.error = error
+
+
 class InvalidHttpRange(InvalidHttpResponse):
 
     _fmt = "Invalid http range %(range)r for %(path)s: %(msg)s"

=== modified file 'bzrlib/plugins/launchpad/lp_registration.py'
--- a/bzrlib/plugins/launchpad/lp_registration.py	2011-12-19 10:58:39 +0000
+++ b/bzrlib/plugins/launchpad/lp_registration.py	2012-01-20 13:07:10 +0000
@@ -56,11 +56,7 @@
 class XMLRPCTransport(xmlrpclib.Transport):
 
     def __init__(self, scheme):
-        # In python2.4 xmlrpclib.Transport is a old-style class, and does not
-        # define __init__, so we check first
-        init = getattr(xmlrpclib.Transport, '__init__', None)
-        if init is not None:
-            init(self)
+        xmlrpclib.Transport.__init__(self)
         self._scheme = scheme
         self._opener = _urllib2_wrappers.Opener()
         self.verbose = 0

=== modified file 'bzrlib/plugins/launchpad/test_lp_directory.py'
--- a/bzrlib/plugins/launchpad/test_lp_directory.py	2011-10-06 06:45:46 +0000
+++ b/bzrlib/plugins/launchpad/test_lp_directory.py	2012-01-20 13:07:10 +0000
@@ -19,6 +19,7 @@
 import os
 import xmlrpclib
 
+import bzrlib
 from bzrlib import (
     debug,
     errors,
@@ -29,6 +30,7 @@
 from bzrlib.directory_service import directories
 from bzrlib.tests import (
     features,
+    ssl_certs,
     TestCaseInTempDir,
     TestCaseWithMemoryTransport
 )
@@ -452,12 +454,15 @@
         tests.TestCase.setUp(self)
         self.server = self.server_class()
         self.server.start_server()
+        self.addCleanup(self.server.stop_server)
         # Ensure we don't clobber env
         self.overrideEnv('BZR_LP_XMLRPC_URL', None)
-
-    def tearDown(self):
-        self.server.stop_server()
-        tests.TestCase.tearDown(self)
+        # Ensure we use the right certificates for https.
+        # FIXME: There should be a better way but the only alternative I can
+        # think of involves carrying the ca_certs through the lp_registration
+        # infrastructure to _urllib2_wrappers... -- vila 2012-01-20
+        bzrlib.global_state.cmdline_overrides._from_cmdline(
+            ['ssl.ca_certs=%s' % ssl_certs.build_path('ca.crt')])
 
     def set_canned_response(self, server, path):
         response_format = '''HTTP/1.1 200 OK\r

=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py	2011-12-24 10:10:59 +0000
+++ b/bzrlib/tests/__init__.py	2012-01-04 23:51:16 +0000
@@ -4002,6 +4002,7 @@
         'bzrlib.tests.test_http',
         'bzrlib.tests.test_http_response',
         'bzrlib.tests.test_https_ca_bundle',
+        'bzrlib.tests.test_https_urllib',
         'bzrlib.tests.test_i18n',
         'bzrlib.tests.test_identitymap',
         'bzrlib.tests.test_ignores',

=== modified file 'bzrlib/tests/https_server.py'
--- a/bzrlib/tests/https_server.py	2011-01-10 22:20:12 +0000
+++ b/bzrlib/tests/https_server.py	2012-01-19 17:14:27 +0000
@@ -49,7 +49,13 @@
         serving = test_server.TestingTCPServerMixin.verify_request(
             self, request, client_address)
         if serving:
-            request.do_handshake()
+            try:
+                request.do_handshake()
+            except ssl.SSLError, e:
+                # FIXME: We proabaly want more tests to capture which ssl
+                # errors are worth reporting but mostly our tests want an https
+                # server that works -- vila 2012-01-19
+                return False
         return serving
 
     def ignored_exceptions_during_shutdown(self, e):

=== modified file 'bzrlib/tests/test_http.py'
--- a/bzrlib/tests/test_http.py	2011-10-12 16:00:13 +0000
+++ b/bzrlib/tests/test_http.py	2012-01-20 12:56:05 +0000
@@ -32,7 +32,6 @@
 import bzrlib
 from bzrlib import (
     bzrdir,
-    cethread,
     config,
     debug,
     errors,
@@ -128,22 +127,29 @@
         ('urllib,http', dict(_activity_server=ActivityHTTPServer,
                             _transport=_urllib.HttpTransport_urllib,)),
         ]
+    if features.pycurl.available():
+        activity_scenarios.append(
+            ('pycurl,http', dict(_activity_server=ActivityHTTPServer,
+                                _transport=PyCurlTransport,)),)
     if features.HTTPSServerFeature.available():
+        # FIXME: Until we have a better way to handle self-signed certificates
+        # (like allowing them in a test specific authentication.conf for
+        # example), we need some specialized pycurl/urllib transport for tests.
+        # -- vila 2012-01-20
+        from bzrlib.tests import (
+            ssl_certs,
+            )
+        class HTTPS_urllib_transport(_urllib.HttpTransport_urllib):
+
+            def __init__(self, base, _from_transport=None):
+                super(HTTPS_urllib_transport, self).__init__(
+                    base, _from_transport=_from_transport,
+                    ca_certs=ssl_certs.build_path('ca.crt'))
+
         activity_scenarios.append(
             ('urllib,https', dict(_activity_server=ActivityHTTPSServer,
-                                _transport=_urllib.HttpTransport_urllib,)),)
-    if features.pycurl.available():
-        activity_scenarios.append(
-            ('pycurl,http', dict(_activity_server=ActivityHTTPServer,
-                                _transport=PyCurlTransport,)),)
-        if features.HTTPSServerFeature.available():
-            from bzrlib.tests import (
-                ssl_certs,
-                )
-            # FIXME: Until we have a better way to handle self-signed
-            # certificates (like allowing them in a test specific
-            # authentication.conf for example), we need some specialized pycurl
-            # transport for tests.
+                                  _transport=HTTPS_urllib_transport,)),)
+        if features.pycurl.available():
             class HTTPS_pycurl_transport(PyCurlTransport):
 
                 def __init__(self, base, _from_transport=None):
@@ -2120,18 +2126,17 @@
         tests.TestCase.setUp(self)
         self.server = self._activity_server(self._protocol_version)
         self.server.start_server()
+        self.addCleanup(self.server.stop_server)
         _activities = {} # Don't close over self and create a cycle
         def report_activity(t, bytes, direction):
             count = _activities.get(direction, 0)
             count += bytes
             _activities[direction] = count
         self.activities = _activities
-
         # We override at class level because constructors may propagate the
         # bound method and render instance overriding ineffective (an
         # alternative would be to define a specific ui factory instead...)
         self.overrideAttr(self._transport, '_report_activity', report_activity)
-        self.addCleanup(self.server.stop_server)
 
     def get_transport(self):
         t = self._transport(self.server.get_url())

=== added file 'bzrlib/tests/test_https_urllib.py'
--- a/bzrlib/tests/test_https_urllib.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/test_https_urllib.py	2012-01-19 15:27:47 +0000
@@ -0,0 +1,109 @@
+# Copyright (C) 2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Tests for the SSL support in the urllib HTTP transport.
+
+"""
+
+import os
+import ssl
+
+from bzrlib import (
+    config,
+    trace,
+    )
+from bzrlib.errors import (
+    CertificateError,
+    ConfigOptionValueError,
+    )
+from bzrlib.tests import (
+    TestCase,
+    TestCaseInTempDir,
+    )
+from bzrlib.transport.http import _urllib2_wrappers
+
+
+class CaCertsConfigTests(TestCaseInTempDir):
+
+    def get_stack(self, content):
+        return config.MemoryStack(content.encode('utf-8'))
+
+    def test_default_raises_value_error(self):
+        stack = self.get_stack("")
+        self.overrideAttr(_urllib2_wrappers, "DEFAULT_CA_PATH",
+                "/i-do-not-exist")
+        self.assertRaises(ValueError, stack.get, 'ssl.ca_certs')
+
+    def test_default_exists(self):
+        self.build_tree(['cacerts.pem'])
+        stack = self.get_stack("")
+        path = os.path.join(self.test_dir, "cacerts.pem")
+        self.overrideAttr(_urllib2_wrappers, "DEFAULT_CA_PATH", path)
+        self.assertEquals(path, stack.get('ssl.ca_certs'))
+
+    def test_specified(self):
+        self.build_tree(['cacerts.pem'])
+        path = os.path.join(self.test_dir, "cacerts.pem")
+        stack = self.get_stack("ssl.ca_certs = %s\n" % path)
+        self.assertEquals(path, stack.get('ssl.ca_certs'))
+
+    def test_specified_doesnt_exist(self):
+        path = os.path.join(self.test_dir, "nonexisting.pem")
+        stack = self.get_stack("ssl.ca_certs = %s\n" % path)
+        self.warnings = []
+        def warning(*args):
+            self.warnings.append(args[0] % args[1:])
+        self.overrideAttr(trace, 'warning', warning)
+        self.assertEquals(_urllib2_wrappers.DEFAULT_CA_PATH,
+                          stack.get('ssl.ca_certs'))
+        self.assertLength(1, self.warnings)
+        self.assertContainsRe(self.warnings[0],
+                              "is not valid for \"ssl.ca_certs\"")
+
+
+class CertReqsConfigTests(TestCaseInTempDir):
+
+    def test_default(self):
+        stack = config.MemoryStack("")
+        self.assertEquals(ssl.CERT_REQUIRED, stack.get("ssl.cert_reqs"))
+
+    def test_from_string(self):
+        stack = config.MemoryStack("ssl.cert_reqs = none\n")
+        self.assertEquals(ssl.CERT_NONE, stack.get("ssl.cert_reqs"))
+        stack = config.MemoryStack("ssl.cert_reqs = optional\n")
+        self.assertEquals(ssl.CERT_OPTIONAL, stack.get("ssl.cert_reqs"))
+        stack = config.MemoryStack("ssl.cert_reqs = required\n")
+        self.assertEquals(ssl.CERT_REQUIRED, stack.get("ssl.cert_reqs"))
+        stack = config.MemoryStack("ssl.cert_reqs = invalid\n")
+        self.assertRaises(ConfigOptionValueError, stack.get, "ssl.cert_reqs")
+
+
+class MatchHostnameTests(TestCase):
+
+    def test_no_certificate(self):
+        self.assertRaises(ValueError,
+                          _urllib2_wrappers.match_hostname, {}, "example.com")
+
+    def test_no_valid_attributes(self):
+        self.assertRaises(CertificateError, _urllib2_wrappers.match_hostname,
+                          {"Problem": "Solved"}, "example.com")
+
+    def test_common_name(self):
+        cert = {'subject': ((('commonName', 'example.com'),),)}
+        self.assertIs(None,
+                      _urllib2_wrappers.match_hostname(cert, "example.com"))
+        self.assertRaises(CertificateError, _urllib2_wrappers.match_hostname,
+                          cert, "example.org")

=== modified file 'bzrlib/transport/__init__.py'
--- a/bzrlib/transport/__init__.py	2011-12-24 10:10:59 +0000
+++ b/bzrlib/transport/__init__.py	2012-01-20 13:15:15 +0000
@@ -1782,7 +1782,7 @@
                  help="Read-only access of branches exported on the web.")
 register_transport_proto('https://',
             help="Read-only access of branches exported on the web using SSL.")
-# The default http implementation is urllib, but https is pycurl if available
+# The default http implementation is urllib, but https uses pycurl if available
 register_lazy_transport('http://', 'bzrlib.transport.http._pycurl',
                         'PyCurlTransport')
 register_lazy_transport('http://', 'bzrlib.transport.http._urllib',

=== modified file 'bzrlib/transport/http/_urllib.py'
--- a/bzrlib/transport/http/_urllib.py	2011-12-18 15:28:38 +0000
+++ b/bzrlib/transport/http/_urllib.py	2012-01-20 09:19:14 +0000
@@ -38,14 +38,14 @@
 
     _opener_class = Opener
 
-    def __init__(self, base, _from_transport=None):
+    def __init__(self, base, _from_transport=None, ca_certs=None):
         super(HttpTransport_urllib, self).__init__(
             base, 'urllib', _from_transport=_from_transport)
         if _from_transport is not None:
             self._opener = _from_transport._opener
         else:
             self._opener = self._opener_class(
-                report_activity=self._report_activity)
+                report_activity=self._report_activity, ca_certs=ca_certs)
 
     def _perform(self, request):
         """Send the request to the server and handles common errors.
@@ -175,7 +175,18 @@
         )
     permutations = [(HttpTransport_urllib, http_server.HttpServer_urllib),]
     if features.HTTPSServerFeature.available():
-        from bzrlib.tests import https_server
-        permutations.append((HttpTransport_urllib,
+        from bzrlib.tests import (
+            https_server,
+            ssl_certs,
+            )
+
+        class HTTPS_urllib_transport(HttpTransport_urllib):
+
+            def __init__(self, base, _from_transport=None):
+                super(HTTPS_urllib_transport, self).__init__(
+                    base, _from_transport=_from_transport,
+                    ca_certs=ssl_certs.build_path('ca.crt'))
+
+        permutations.append((HTTPS_urllib_transport,
                              https_server.HTTPSServer_urllib))
     return permutations

=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
--- a/bzrlib/transport/http/_urllib2_wrappers.py	2011-12-19 13:23:58 +0000
+++ b/bzrlib/transport/http/_urllib2_wrappers.py	2012-01-20 09:19:14 +0000
@@ -14,7 +14,7 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-"""Implementaion of urllib2 tailored to bzr needs
+"""Implementation of urllib2 tailored to bzr needs
 
 This file complements the urllib2 class hierarchy with custom classes.
 
@@ -50,6 +50,7 @@
 
 import errno
 import httplib
+import os
 import socket
 import urllib
 import urllib2
@@ -63,12 +64,68 @@
     config,
     debug,
     errors,
+    lazy_import,
     osutils,
     trace,
     transport,
     urlutils,
     )
-
+lazy_import.lazy_import(globals(), """
+import ssl
+""")
+
+DEFAULT_CA_PATH = u"/etc/ssl/certs/ca-certificates.crt"
+
+
+def default_ca_certs():
+    if not os.path.exists(DEFAULT_CA_PATH):
+        raise ValueError("default ca certs path %s does not exist" %
+            DEFAULT_CA_PATH)
+    return DEFAULT_CA_PATH
+
+
+def ca_certs_from_store(path):
+    if not os.path.exists(path):
+        raise ValueError("ca certs path %s does not exist" % path)
+    return path
+
+
+def default_cert_reqs():
+    return u"required"
+
+
+def cert_reqs_from_store(unicode_str):
+    import ssl
+    try:
+        return {
+            "required": ssl.CERT_REQUIRED,
+            "optional": ssl.CERT_OPTIONAL,
+            "none": ssl.CERT_NONE
+            }[unicode_str]
+    except KeyError:
+        raise ValueError("invalid value %s" % unicode_str)
+
+
+opt_ssl_ca_certs = config.Option('ssl.ca_certs',
+        from_unicode=ca_certs_from_store,
+        default=default_ca_certs,
+        invalid='warning',
+        help="""\
+Path to certification authority certificates to trust.
+""")
+
+opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
+        default=default_cert_reqs,
+        from_unicode=cert_reqs_from_store,
+        invalid='error',
+        help="""\
+Whether to require a certificate from the remote side. (default:required)
+
+Possible values:
+ * none: Certificates ignored
+ * optional: Certificates not required, but validated if provided
+ * required: Certificates required, and validated
+""")
 
 checked_kerberos = False
 kerberos = None
@@ -299,11 +356,12 @@
 
     # XXX: Needs refactoring at the caller level.
     def __init__(self, host, port=None, proxied_host=None,
-                 report_activity=None):
+                 report_activity=None, ca_certs=None):
         AbstractHTTPConnection.__init__(self, report_activity=report_activity)
         # Use strict=True since we don't support HTTP/0.9
         httplib.HTTPConnection.__init__(self, host, port, strict=True)
         self.proxied_host = proxied_host
+        # ca_certs is ignored, it's only relevant for https
 
     def connect(self):
         if 'http' in debug.debug_flags:
@@ -312,29 +370,72 @@
         self._wrap_socket_for_reporting(self.sock)
 
 
-# Build the appropriate socket wrapper for ssl
-try:
-    # python 2.6 introduced a better ssl package
-    import ssl
-    _ssl_wrap_socket = ssl.wrap_socket
-except ImportError:
-    # python versions prior to 2.6 don't have ssl and ssl.wrap_socket instead
-    # they use httplib.FakeSocket
-    def _ssl_wrap_socket(sock, key_file, cert_file):
-        ssl_sock = socket.ssl(sock, key_file, cert_file)
-        return httplib.FakeSocket(sock, ssl_sock)
+# These two methods were imported from Python 3.2's ssl module
+
+def _dnsname_to_pat(dn):
+    pats = []
+    for frag in dn.split(r'.'):
+        if frag == '*':
+            # When '*' is a fragment by itself, it matches a non-empty dotless
+            # fragment.
+            pats.append('[^.]+')
+        else:
+            # Otherwise, '*' matches any dotless fragment.
+            frag = re.escape(frag)
+            pats.append(frag.replace(r'\*', '[^.]*'))
+    return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
+
+
+def match_hostname(cert, hostname):
+    """Verify that *cert* (in decoded format as returned by
+    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 rules
+    are mostly followed, but IP addresses are not accepted for *hostname*.
+
+    CertificateError is raised on failure. On success, the function
+    returns nothing.
+    """
+    if not cert:
+        raise ValueError("empty or no certificate")
+    dnsnames = []
+    san = cert.get('subjectAltName', ())
+    for key, value in san:
+        if key == 'DNS':
+            if _dnsname_to_pat(value).match(hostname):
+                return
+            dnsnames.append(value)
+    if not san:
+        # The subject is only checked when subjectAltName is empty
+        for sub in cert.get('subject', ()):
+            for key, value in sub:
+                # XXX according to RFC 2818, the most specific Common Name
+                # must be used.
+                if key == 'commonName':
+                    if _dnsname_to_pat(value).match(hostname):
+                        return
+                    dnsnames.append(value)
+    if len(dnsnames) > 1:
+        raise errors.CertificateError(
+            "hostname %r doesn't match either of %s"
+            % (hostname, ', '.join(map(repr, dnsnames))))
+    elif len(dnsnames) == 1:
+        raise errors.CertificateError("hostname %r doesn't match %r" %
+                                      (hostname, dnsnames[0]))
+    else:
+        raise errors.CertificateError("no appropriate commonName or "
+            "subjectAltName fields were found")
 
 
 class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
 
     def __init__(self, host, port=None, key_file=None, cert_file=None,
                  proxied_host=None,
-                 report_activity=None):
+                 report_activity=None, ca_certs=None):
         AbstractHTTPConnection.__init__(self, report_activity=report_activity)
         # Use strict=True since we don't support HTTP/0.9
         httplib.HTTPSConnection.__init__(self, host, port,
                                          key_file, cert_file, strict=True)
         self.proxied_host = proxied_host
+        self.ca_certs = ca_certs
 
     def connect(self):
         if 'http' in debug.debug_flags:
@@ -345,7 +446,38 @@
             self.connect_to_origin()
 
     def connect_to_origin(self):
-        ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
+        # FIXME JRV 2011-12-18: Use location config here?
+        config_stack = config.GlobalStack()
+        if self.ca_certs is None:
+            ca_certs = config_stack.get('ssl.ca_certs')
+        else:
+            ca_certs = self.ca_certs
+        cert_reqs = config_stack.get('ssl.cert_reqs')
+        if cert_reqs == ssl.CERT_NONE:
+            trace.warning("not checking SSL certificates for %s: %d",
+                self.host, self.port)
+        else:
+            if ca_certs is None:
+                trace.warning(
+                    "no valid trusted SSL CA certificates file set. See "
+                    "'bzr help ssl.ca_certs' for more information on setting "
+                    "trusted CA's.")
+        try:
+            ssl_sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file,
+                cert_reqs=cert_reqs, ca_certs=ca_certs)
+        except ssl.SSLError, e:
+            if e.errno != ssl.SSL_ERROR_SSL:
+                raise
+            trace.note(
+                "To disable SSL certificate verification, use "
+                "-Ossl.cert_reqs=none. See ``bzr help ssl.ca_certs`` for "
+                "more information on specifying trusted CA certificates.")
+            raise
+        peer_cert = ssl_sock.getpeercert()
+        if (cert_reqs == ssl.CERT_REQUIRED or
+            (cert_reqs == ssl.CERT_OPTIONAL and peer_cert)):
+            match_hostname(peer_cert, self.host)
+
         # Wrap the ssl socket before anybody use it
         self._wrap_socket_for_reporting(ssl_sock)
 
@@ -453,8 +585,9 @@
 
     handler_order = 1000 # after all pre-processings
 
-    def __init__(self, report_activity=None):
+    def __init__(self, report_activity=None, ca_certs=None):
         self._report_activity = report_activity
+        self.ca_certs = ca_certs
 
     def create_connection(self, request, http_connection_class):
         host = request.get_host()
@@ -468,7 +601,8 @@
         try:
             connection = http_connection_class(
                 host, proxied_host=request.proxied_host,
-                report_activity=self._report_activity)
+                report_activity=self._report_activity,
+                ca_certs=self.ca_certs)
         except httplib.InvalidURL, exception:
             # There is only one occurrence of InvalidURL in httplib
             raise errors.InvalidURL(request.get_full_url(),
@@ -660,6 +794,10 @@
                     % (request, request.connection.sock.getsockname())
             response = connection.getresponse()
             convert_to_addinfourl = True
+        except (ssl.SSLError, errors.CertificateError):
+            # Something is wrong with either the certificate or the hostname,
+            # re-trying won't help
+            raise
         except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
                 socket.error, httplib.HTTPException):
             response = self.retry_or_raise(http_class, request, first_try)
@@ -1657,9 +1795,10 @@
                  connection=ConnectionHandler,
                  redirect=HTTPRedirectHandler,
                  error=HTTPErrorProcessor,
-                 report_activity=None):
+                 report_activity=None,
+                 ca_certs=None):
         self._opener = urllib2.build_opener(
-            connection(report_activity=report_activity),
+            connection(report_activity=report_activity, ca_certs=ca_certs),
             redirect, error,
             ProxyHandler(),
             HTTPBasicAuthHandler(),

=== modified file 'doc/en/release-notes/bzr-2.5.txt'
--- a/doc/en/release-notes/bzr-2.5.txt	2012-01-20 15:26:27 +0000
+++ b/doc/en/release-notes/bzr-2.5.txt	2012-01-20 16:42:27 +0000
@@ -53,6 +53,10 @@
 * Test for equality instead of object identity where ROOT_PARENT is concerned.
   (Wouter van Heyst, #881142)
 
+* urllib-based HTTPS client connections now verify the server certificate
+  validity as well as the hostname.
+  (Jelmer Vernooij, Vincent Ladeuil, #651161)
+
 Documentation
 *************
 

=== modified file 'doc/en/whats-new/whats-new-in-2.5.txt'
--- a/doc/en/whats-new/whats-new-in-2.5.txt	2012-01-03 16:28:43 +0000
+++ b/doc/en/whats-new/whats-new-in-2.5.txt	2012-01-04 23:51:16 +0000
@@ -36,6 +36,23 @@
 encoding for interacting with the filesystem. This makes creating a working
 tree and commiting to it possible for such branches in most environments.
 
+SSL Certificate Verification Support in urllib HTTPS backend
+************************************************************
+
+In previous versions of Bazaar, only one of the two supported HTTPS
+backends, pycurl, supported verification of SSL certificates. This version
+also introduces this support for the urllib backend.
+
+Along with this support two new options have been introduced to control
+which CA's are trusted and to what degree server certificates should be
+verified. See ``bzr help ssl.ca_certs`` and ``bzr help ssl.cert_reqs``
+for more information
+
+Users who have previously used the urllib HTTPS backend with servers
+with invalid or untrusted certificates can continue to do so by
+adding the required certificates to the trusted CA certificate file
+(recommended) or by setting the ``ssl.cert_reqs`` option to ``none``.
+
 Faster smart server
 *******************
 




More information about the bazaar-commits mailing list