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