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

Proposed by Jelmer Vernooij
Status: Merged
Merge reported by: Vincent Ladeuil
Merged at revision: not available
Proposed branch: lp:~jelmer/bzr/urllib-verifies-ssl-certs
Merge into: lp:bzr
Diff against target: 441 lines (+293/-18)
9 files modified
bzrlib/config.py (+7/-0)
bzrlib/errors.py (+8/-0)
bzrlib/tests/__init__.py (+1/-0)
bzrlib/tests/test_http.py (+0/-1)
bzrlib/tests/test_https_urllib.py (+109/-0)
bzrlib/transport/__init__.py (+3/-3)
bzrlib/transport/http/_urllib2_wrappers.py (+142/-14)
doc/en/release-notes/bzr-2.5.txt (+6/-0)
doc/en/whats-new/whats-new-in-2.5.txt (+17/-0)
To merge this branch: bzr merge lp:~jelmer/bzr/urllib-verifies-ssl-certs
Reviewer Review Type Date Requested Status
Vincent Ladeuil Needs Fixing
Martin Packman (community) Approve
Review via email: mp+87380@code.launchpad.net

This proposal supersedes a proposal from 2011-12-20.

Commit message

Support verification of SSL certificates for the urllib HTTPS backend.

Description of the change

Add support for verifying SSL certificates when using urllib.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

On 4 November 2011 14:03, Jelmer Vernooij <email address hidden> wrote:
> Jelmer Vernooij has proposed merging lp:~jelmer/bzr/urllib-verifies-ssl-certs into lp:bzr.
>
> Requested reviews:
>  bzr-core (bzr-core)
> Related bugs:
>  Bug #125055 in Bazaar: "defaulting to pycurl doesn't make sense anymore"
>  https://bugs.launchpad.net/bzr/+bug/125055
>  Bug #651161 in Bazaar: "bzr fails to verify ssl validity in https connections - by default --> as pycurl isn't a dep only a suggestion "
>  https://bugs.launchpad.net/bzr/+bug/651161
>
> For more details, see:
> https://code.launchpad.net/~jelmer/bzr/urllib-verifies-ssl-certs/+merge/81227
>
> Add support for verifying SSL certificates when using urllib.

That's great, thanks.

> This branch does actually work, but it's got some open ends:
>
> * The location with the CAs to trust differs per system. Rather than hardcoding
>  a list, I think we should have a configuration option that allows the user
>  to specify what CA list they want to verify against.
>
>  This option could then also be pointed at a user-specific list
>  (rather than a system wide list). And packagers can put in the default
>  for their distribution.
>
>  Where do I obtain this option from, which config stack ?

[needsfixing] I think the global stack would be a good place to start
- it would seem like a kind of contrived case to want different values
on a smaller scope. We can always change it later.

> * We probably want some way to specify that self-signed certificates are ok.
>  Probably also a new option? We should probably still warn the user
>  when a self-signed certificate is being trusted.

+1 to a new option and to always warning.

Though perhaps it would be equivalent and easier to just have an
option saying "don't do validation."

> * There are no additional tests. There probably should be some, at least for
>  e.g. the match_hostname helper.

It would be nice, if it's not too hard, to actually test against an
untrusted cert, or at least to do monkeypatching to make it look like
we got an invalid cert.

[needsfixing] I'd like a unit test for match_hostname covering the
various cases.

> * I have the feeling that I'm missing something. This was way too easy.
>  What is it?

I haven't read the rfc but it looks plausible. You could wait for a
review from vila, who has probably touched this most, or robert who is
an http nerd.

I wonder if this needs a change to Windows or Mac packaging to make
sure the certs are there. Or, possibly we can get them from the
system's keyring in some way.

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal
Download full text (4.0 KiB)

> Add support for verifying SSL certificates when using urllib.

\o/

>
> This branch does actually work, but it's got some open ends:
>
> * The location with the CAs to trust differs per system. Rather than hardcoding
> a list, I think we should have a configuration option that allows the user
> to specify what CA list they want to verify against.

pycurl already handles that for windows but getting it right in the general
case... sounds like a packaging issue :-/

>
> This option could then also be pointed at a user-specific list
> (rather than a system wide list). And packagers can put in the default
> for their distribution.
>
> Where do I obtain this option from, which config stack ?

I'd go with global to start with specifying the default value at
registration time if possible.

Arguably it's a system-wide config file that should be used, making packager
life easier.

>
> * We probably want some way to specify that self-signed certificates are ok.
> Probably also a new option? We should probably still warn the user
> when a self-signed certificate is being trusted.

authentication.conf is the right place for that and the option is even
documented but not implemented ;)

>
> * There are no additional tests. There probably should be some, at least for
> e.g. the match_hostname helper.

Yup. There are already https tests for pycurl that should be activated now
(<broken record> hey, did I mention the 'scenarios' attribute is cool for
that ? </broken record> ;)

>
> * I have the feeling that I'm missing something. This was way too easy.
> What is it?

Hehe.

The last time I worked on this area we were still supporting python-2.4/2.5
where https wasn't supported. Now, you also found the relevant code in
python-3.2 so... that certainly simplify things *a lot* :)

This wasn't a full review but as I understand it you weren't requesting one
either ;)

As for tests, if you look at the pycurl ones you'll certainly find that most
of the needed infrastructure is there to build certificates for special
purposes (self-signed, obsolete, etc) and easily setup an https server.

99 + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
100 + are mostly followed, but IP addresses are not accepted for *hostname*.

Hmm, IP addresses not accepted ? I bet our tests won't like that much.

pycurl itself was not open enough to test for these weird certificates AFAIR
but it's also a robust implementation so I wasn't concerned, but if we make
urllib the default one, we have to make sure we're making the right checks.

I'll need to dig a bit more to find back the relevant discussions about this
topic.

Also, defaulting to urllib is fine but the documentation probably needs to
be updated (created even ?) to make it clear that we're not defaulting to
pycurl. We had issues in the past (including failures to check lp
certificate) which had led to the actual defaults (urllib for http, pycurl
for https), let's make the switch crystal clear.

141 def connect_to_origin(self):
142 - ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
143 + ssl_sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file,
144 + ...

Read more...

Revision history for this message
Martin Packman (gz) wrote : Posted in a previous version of this proposal

I think there's general agreement this is great, and with the reqirement of Python 2.6 something that's now acheivable and should probably get into the next bzr release.

It absolutely needs some kind of automated testing though, and likely an option for disabling checks, I've used `curl -k` and `wget --no-check-certificate` enough to know I'd be annoyed if that option wasn't there.

review: Needs Fixing
Revision history for this message
Martin Packman (gz) wrote : Posted in a previous version of this proposal

Summarising from earlier rambling chat:

* The error shown when a certificate can't be checked because no local store is found needs to spell out the `-Ossl.cert_reqs=none` parameter for getting the old insecure behaviour, and maybe a pointer to more help.
* Perhaps the error shown when a self-signed certificate doesn't validate against the local store also wants some love.
* An entry in doc/en/whats-new/whats-new-in-2.5.txt needs adding spelling out the user-facing aspects of this change, as it's a nice improvement but some people may need to change their configuration on upgrading.

The mercurial wiki page linked earlier is a useful comparison:
<http://mercurial.selenic.com/wiki/CACertificates>

review: Needs Fixing
Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

> Summarising from earlier rambling chat:
>
> * The error shown when a certificate can't be checked because no local store
> is found needs to spell out the `-Ossl.cert_reqs=none` parameter for getting
> the old insecure behaviour, and maybe a pointer to more help.
> * Perhaps the error shown when a self-signed certificate doesn't validate
> against the local store also wants some love.
> * An entry in doc/en/whats-new/whats-new-in-2.5.txt needs adding spelling out
> the user-facing aspects of this change, as it's a nice improvement but some
> people may need to change their configuration on upgrading.

Done.

Ironically I can't actually run "bzr selftest -s bt.test_http" on my machine because pycurl triggers segfaults (not just on this branch), but the changes are mostly documentation anyway.

Revision history for this message
Martin Packman (gz) wrote :

Added what's new text is great, and the new logic for printing helpful messages seems good. Some of these messages could probably do with gettext() added, but that can wait for another branch when the rest of the module can be looked over too.

- if cert_reqs == "none":
+ if cert_reqs == ssl.CERT_NONE:

Seen in the interdiff for the last change, was this broken before, and did a test fail because of it?

review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

sent to pqm by email

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

sent to pqm by email

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

sent to pqm by email

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

> Added what's new text is great, and the new logic for printing helpful
> messages seems good. Some of these messages could probably do with gettext()
> added, but that can wait for another branch when the rest of the module can be
> looked over too.
>
> - if cert_reqs == "none":
> + if cert_reqs == ssl.CERT_NONE:
>
> Seen in the interdiff for the last change, was this broken before, and did a
> test fail because of it?
Yeah, it was indeed broken - and I hadn't noticed since we don't have a test for it. There is an open bug about doing more testing with SSL certificates.

Revision history for this message
Vincent Ladeuil (vila) wrote :

Will look into it asap but at least the news needs to be fixed.

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/config.py'
2--- bzrlib/config.py 2012-01-05 14:10:32 +0000
3+++ bzrlib/config.py 2012-01-07 00:08:23 +0000
4@@ -2867,6 +2867,13 @@
5 Option('submit_to',
6 help='''Where submissions from this branch are mailed to.'''))
7
8+option_registry.register_lazy('ssl.ca_certs',
9+ 'bzrlib.transport.http._urllib2_wrappers', 'opt_ssl_ca_certs')
10+
11+option_registry.register_lazy('ssl.cert_reqs',
12+ 'bzrlib.transport.http._urllib2_wrappers', 'opt_ssl_cert_reqs')
13+
14+
15
16 class Section(object):
17 """A section defines a dict of option name => value.
18
19=== modified file 'bzrlib/errors.py'
20--- bzrlib/errors.py 2011-12-22 15:33:16 +0000
21+++ bzrlib/errors.py 2012-01-07 00:08:23 +0000
22@@ -1667,6 +1667,14 @@
23 TransportError.__init__(self, msg, orig_error=orig_error)
24
25
26+class CertificateError(TransportError):
27+
28+ _fmt = "Certificate error: %(error)s"
29+
30+ def __init__(self, error):
31+ self.error = error
32+
33+
34 class InvalidHttpRange(InvalidHttpResponse):
35
36 _fmt = "Invalid http range %(range)r for %(path)s: %(msg)s"
37
38=== modified file 'bzrlib/tests/__init__.py'
39--- bzrlib/tests/__init__.py 2011-12-24 10:10:59 +0000
40+++ bzrlib/tests/__init__.py 2012-01-07 00:08:23 +0000
41@@ -4002,6 +4002,7 @@
42 'bzrlib.tests.test_http',
43 'bzrlib.tests.test_http_response',
44 'bzrlib.tests.test_https_ca_bundle',
45+ 'bzrlib.tests.test_https_urllib',
46 'bzrlib.tests.test_i18n',
47 'bzrlib.tests.test_identitymap',
48 'bzrlib.tests.test_ignores',
49
50=== modified file 'bzrlib/tests/test_http.py'
51--- bzrlib/tests/test_http.py 2011-10-12 16:00:13 +0000
52+++ bzrlib/tests/test_http.py 2012-01-07 00:08:23 +0000
53@@ -32,7 +32,6 @@
54 import bzrlib
55 from bzrlib import (
56 bzrdir,
57- cethread,
58 config,
59 debug,
60 errors,
61
62=== added file 'bzrlib/tests/test_https_urllib.py'
63--- bzrlib/tests/test_https_urllib.py 1970-01-01 00:00:00 +0000
64+++ bzrlib/tests/test_https_urllib.py 2012-01-07 00:08:23 +0000
65@@ -0,0 +1,109 @@
66+# Copyright (C) 2011 Canonical Ltd
67+#
68+# This program is free software; you can redistribute it and/or modify
69+# it under the terms of the GNU General Public License as published by
70+# the Free Software Foundation; either version 2 of the License, or
71+# (at your option) any later version.
72+#
73+# This program is distributed in the hope that it will be useful,
74+# but WITHOUT ANY WARRANTY; without even the implied warranty of
75+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
76+# GNU General Public License for more details.
77+#
78+# You should have received a copy of the GNU General Public License
79+# along with this program; if not, write to the Free Software
80+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
81+
82+"""Tests for the SSL support in the urllib HTTP transport.
83+
84+"""
85+
86+import ssl
87+
88+from bzrlib import trace
89+from bzrlib.errors import (
90+ CertificateError,
91+ ConfigOptionValueError,
92+ )
93+from bzrlib.config import (
94+ IniFileStore,
95+ Stack,
96+ )
97+import os
98+from bzrlib.tests import (
99+ TestCase,
100+ TestCaseInTempDir,
101+ )
102+from bzrlib.transport.http import _urllib2_wrappers
103+
104+
105+def stack_from_string(text):
106+ store = IniFileStore()
107+ store._load_from_string(text)
108+ return Stack([store.get_sections])
109+
110+
111+class CaCertsConfigTests(TestCaseInTempDir):
112+
113+ def test_default_raises_value_error(self):
114+ stack = stack_from_string("")
115+ self.overrideAttr(_urllib2_wrappers, "DEFAULT_CA_PATH",
116+ "/i-do-not-exist")
117+ self.assertRaises(ValueError, stack.get, 'ssl.ca_certs')
118+
119+ def test_default_exists(self):
120+ self.build_tree(['cacerts.pem'])
121+ stack = stack_from_string("")
122+ path = os.path.join(self.test_dir, "cacerts.pem")
123+ self.overrideAttr(_urllib2_wrappers, "DEFAULT_CA_PATH", path)
124+ self.assertEquals(path, stack.get('ssl.ca_certs'))
125+
126+ def test_specified(self):
127+ self.build_tree(['cacerts.pem'])
128+ path = os.path.join(self.test_dir, "cacerts.pem")
129+ stack = stack_from_string("ssl.ca_certs = %s\n" % path)
130+ self.assertEquals(path, stack.get('ssl.ca_certs'))
131+
132+ def test_specified_doesnt_exist(self):
133+ path = os.path.join(self.test_dir, "nonexisting.pem")
134+ stack = stack_from_string("ssl.ca_certs = %s\n" % path)
135+ self.warnings = []
136+ def warning(*args):
137+ self.warnings.append(args[0] % args[1:])
138+ self.overrideAttr(trace, 'warning', warning)
139+ self.assertEquals(_urllib2_wrappers.DEFAULT_CA_PATH, stack.get('ssl.ca_certs'))
140+ self.assertLength(1, self.warnings)
141+ self.assertContainsRe(self.warnings[0], "is not valid for \"ssl.ca_certs\"")
142+
143+
144+class CertReqsConfigTests(TestCaseInTempDir):
145+
146+ def test_default(self):
147+ stack = stack_from_string("")
148+ self.assertEquals(ssl.CERT_REQUIRED, stack.get("ssl.cert_reqs"))
149+
150+ def test_from_string(self):
151+ stack = stack_from_string("ssl.cert_reqs = none\n")
152+ self.assertEquals(ssl.CERT_NONE, stack.get("ssl.cert_reqs"))
153+ stack = stack_from_string("ssl.cert_reqs = optional\n")
154+ self.assertEquals(ssl.CERT_OPTIONAL, stack.get("ssl.cert_reqs"))
155+ stack = stack_from_string("ssl.cert_reqs = required\n")
156+ self.assertEquals(ssl.CERT_REQUIRED, stack.get("ssl.cert_reqs"))
157+ stack = stack_from_string("ssl.cert_reqs = invalid\n")
158+ self.assertRaises(ConfigOptionValueError, stack.get, "ssl.cert_reqs")
159+
160+
161+class MatchHostnameTests(TestCase):
162+
163+ def test_no_certificate(self):
164+ self.assertRaises(ValueError, _urllib2_wrappers.match_hostname({}))
165+
166+ def test_no_valid_attributes(self):
167+ self.assertRaises(CertificateError,
168+ _urllib2_wrappers.match_hostname({"Problem": "Solved"}))
169+
170+ def test_common_name(self):
171+ cert = {'subject': ((('commonName', 'example.com'),),)}
172+ self.assertIs(None, _urllib2_wrappers.match_hostname(cert, "example.com"))
173+ self.assertRaises(CertificateError, _urllib2_wrappers.match_hostname,
174+ cert, "example.org")
175
176=== modified file 'bzrlib/transport/__init__.py'
177--- bzrlib/transport/__init__.py 2011-12-24 10:10:59 +0000
178+++ bzrlib/transport/__init__.py 2012-01-07 00:08:23 +0000
179@@ -1782,15 +1782,15 @@
180 help="Read-only access of branches exported on the web.")
181 register_transport_proto('https://',
182 help="Read-only access of branches exported on the web using SSL.")
183-# The default http implementation is urllib, but https is pycurl if available
184+# The default http implementation is urllib
185 register_lazy_transport('http://', 'bzrlib.transport.http._pycurl',
186 'PyCurlTransport')
187+register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
188+ 'PyCurlTransport')
189 register_lazy_transport('http://', 'bzrlib.transport.http._urllib',
190 'HttpTransport_urllib')
191 register_lazy_transport('https://', 'bzrlib.transport.http._urllib',
192 'HttpTransport_urllib')
193-register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
194- 'PyCurlTransport')
195
196 register_transport_proto('ftp://', help="Access using passive FTP.")
197 register_lazy_transport('ftp://', 'bzrlib.transport.ftp', 'FtpTransport')
198
199=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
200--- bzrlib/transport/http/_urllib2_wrappers.py 2011-12-19 13:23:58 +0000
201+++ bzrlib/transport/http/_urllib2_wrappers.py 2012-01-07 00:08:23 +0000
202@@ -14,7 +14,7 @@
203 # along with this program; if not, write to the Free Software
204 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
205
206-"""Implementaion of urllib2 tailored to bzr needs
207+"""Implementation of urllib2 tailored to bzr needs
208
209 This file complements the urllib2 class hierarchy with custom classes.
210
211@@ -50,6 +50,7 @@
212
213 import errno
214 import httplib
215+import os
216 import socket
217 import urllib
218 import urllib2
219@@ -63,12 +64,68 @@
220 config,
221 debug,
222 errors,
223+ lazy_import,
224 osutils,
225 trace,
226 transport,
227 urlutils,
228 )
229-
230+lazy_import.lazy_import(globals(), """
231+import ssl
232+""")
233+
234+DEFAULT_CA_PATH = u"/etc/ssl/certs/ca-certificates.crt"
235+
236+
237+def default_ca_certs():
238+ if not os.path.exists(DEFAULT_CA_PATH):
239+ raise ValueError("default ca certs path %s does not exist" %
240+ DEFAULT_CA_PATH)
241+ return DEFAULT_CA_PATH
242+
243+
244+def ca_certs_from_store(path):
245+ if not os.path.exists(path):
246+ raise ValueError("ca certs path %s does not exist" % path)
247+ return path
248+
249+
250+def default_cert_reqs():
251+ return u"required"
252+
253+
254+def cert_reqs_from_store(unicode_str):
255+ import ssl
256+ try:
257+ return {
258+ "required": ssl.CERT_REQUIRED,
259+ "optional": ssl.CERT_OPTIONAL,
260+ "none": ssl.CERT_NONE
261+ }[unicode_str]
262+ except KeyError:
263+ raise ValueError("invalid value %s" % unicode_str)
264+
265+
266+opt_ssl_ca_certs = config.Option('ssl.ca_certs',
267+ from_unicode=ca_certs_from_store,
268+ default=default_ca_certs,
269+ invalid='warning',
270+ help="""\
271+Path to certification authority certificates to trust.
272+""")
273+
274+opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
275+ default=default_cert_reqs,
276+ from_unicode=cert_reqs_from_store,
277+ invalid='error',
278+ help="""\
279+Whether to require a certificate from the remote side. (default:required)
280+
281+Possible values:
282+ * none: certificates ignored
283+ * optional: Certificates not required, but validated if provided
284+ * required: Certificates required, and validated
285+""")
286
287 checked_kerberos = False
288 kerberos = None
289@@ -312,17 +369,60 @@
290 self._wrap_socket_for_reporting(self.sock)
291
292
293-# Build the appropriate socket wrapper for ssl
294-try:
295- # python 2.6 introduced a better ssl package
296- import ssl
297- _ssl_wrap_socket = ssl.wrap_socket
298-except ImportError:
299- # python versions prior to 2.6 don't have ssl and ssl.wrap_socket instead
300- # they use httplib.FakeSocket
301- def _ssl_wrap_socket(sock, key_file, cert_file):
302- ssl_sock = socket.ssl(sock, key_file, cert_file)
303- return httplib.FakeSocket(sock, ssl_sock)
304+# These two methods were imported from Python 3.2's ssl module
305+
306+def _dnsname_to_pat(dn):
307+ pats = []
308+ for frag in dn.split(r'.'):
309+ if frag == '*':
310+ # When '*' is a fragment by itself, it matches a non-empty dotless
311+ # fragment.
312+ pats.append('[^.]+')
313+ else:
314+ # Otherwise, '*' matches any dotless fragment.
315+ frag = re.escape(frag)
316+ pats.append(frag.replace(r'\*', '[^.]*'))
317+ return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
318+
319+
320+def match_hostname(cert, hostname):
321+ """Verify that *cert* (in decoded format as returned by
322+ SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
323+ are mostly followed, but IP addresses are not accepted for *hostname*.
324+
325+ CertificateError is raised on failure. On success, the function
326+ returns nothing.
327+ """
328+ if not cert:
329+ raise ValueError("empty or no certificate")
330+ dnsnames = []
331+ san = cert.get('subjectAltName', ())
332+ for key, value in san:
333+ if key == 'DNS':
334+ if _dnsname_to_pat(value).match(hostname):
335+ return
336+ dnsnames.append(value)
337+ if not san:
338+ # The subject is only checked when subjectAltName is empty
339+ for sub in cert.get('subject', ()):
340+ for key, value in sub:
341+ # XXX according to RFC 2818, the most specific Common Name
342+ # must be used.
343+ if key == 'commonName':
344+ if _dnsname_to_pat(value).match(hostname):
345+ return
346+ dnsnames.append(value)
347+ if len(dnsnames) > 1:
348+ raise errors.CertificateError("hostname %r "
349+ "doesn't match either of %s"
350+ % (hostname, ', '.join(map(repr, dnsnames))))
351+ elif len(dnsnames) == 1:
352+ raise errors.CertificateError("hostname %r "
353+ "doesn't match %r"
354+ % (hostname, dnsnames[0]))
355+ else:
356+ raise errors.CertificateError("no appropriate commonName or "
357+ "subjectAltName fields were found")
358
359
360 class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
361@@ -345,7 +445,35 @@
362 self.connect_to_origin()
363
364 def connect_to_origin(self):
365- ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
366+ # FIXME JRV 2011-12-18: Use location config here?
367+ config_stack = config.GlobalStack()
368+ ca_certs = config_stack.get('ssl.ca_certs')
369+ cert_reqs = config_stack.get('ssl.cert_reqs')
370+ if cert_reqs == ssl.CERT_NONE:
371+ trace.warning("not checking SSL certificates for %s: %d",
372+ self.host, self.port)
373+ else:
374+ if ca_certs is None:
375+ trace.warning(
376+ "no valid trusted SSL CA certificates file set. See "
377+ "'bzr help ssl.ca_certs' for more information on setting "
378+ "trusted CA's.")
379+ try:
380+ ssl_sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file,
381+ cert_reqs=cert_reqs, ca_certs=ca_certs)
382+ except ssl.SSLError, e:
383+ if e.errno != ssl.SSL_ERROR_SSL:
384+ raise
385+ trace.note(
386+ "To disable SSL certificate verification, use "
387+ "-Ossl.cert_reqs=none. See ``bzr help ssl.ca_certs`` for "
388+ "more information on specifying trusted CA certificates.")
389+ raise
390+ peer_cert = ssl_sock.getpeercert()
391+ if (cert_reqs == ssl.CERT_REQUIRED or
392+ (cert_reqs == ssl.CERT_OPTIONAL and peer_cert)):
393+ match_hostname(peer_cert, self.host)
394+
395 # Wrap the ssl socket before anybody use it
396 self._wrap_socket_for_reporting(ssl_sock)
397
398
399=== modified file 'doc/en/release-notes/bzr-2.5.txt'
400--- doc/en/release-notes/bzr-2.5.txt 2012-01-06 14:09:04 +0000
401+++ doc/en/release-notes/bzr-2.5.txt 2012-01-07 00:08:23 +0000
402@@ -479,6 +479,12 @@
403 to 'line-based'. This replace the previous shelf UI only patch using
404 ``INSIDE_EMACS``. (Benoît Pierre)
405
406+* urllib-based HTTPS client connections now verify the server certificate
407+ as well as the hostname. (Jelmer Vernooij)
408+
409+* urllib-based HTTPS connections are now the default.
410+ (Jelmer Vernooij, #125055, #651161)
411+
412 Bug Fixes
413 *********
414
415
416=== modified file 'doc/en/whats-new/whats-new-in-2.5.txt'
417--- doc/en/whats-new/whats-new-in-2.5.txt 2012-01-03 16:28:43 +0000
418+++ doc/en/whats-new/whats-new-in-2.5.txt 2012-01-07 00:08:23 +0000
419@@ -36,6 +36,23 @@
420 encoding for interacting with the filesystem. This makes creating a working
421 tree and commiting to it possible for such branches in most environments.
422
423+SSL Certificate Verification Support in urllib HTTPS backend
424+************************************************************
425+
426+In previous versions of Bazaar, only one of the two supported HTTPS
427+backends, pycurl, supported verification of SSL certificates. This version
428+also introduces this support for the urllib backend.
429+
430+Along with this support two new options have been introduced to control
431+which CA's are trusted and to what degree server certificates should be
432+verified. See ``bzr help ssl.ca_certs`` and ``bzr help ssl.cert_reqs``
433+for more information
434+
435+Users who have previously used the urllib HTTPS backend with servers
436+with invalid or untrusted certificates can continue to do so by
437+adding the required certificates to the trusted CA certificate file
438+(recommended) or by setting the ``ssl.cert_reqs`` option to ``none``.
439+
440 Faster smart server
441 *******************
442