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

Proposed by Jelmer Vernooij
Status: Superseded
Proposed branch: lp:~jelmer/bzr/urllib-verifies-ssl-certs
Merge into: lp:bzr
Diff against target: 460 lines (+305/-18) (has conflicts)
9 files modified
bzrlib/config.py (+20/-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 (+141/-14)
doc/en/release-notes/bzr-2.5.txt (+6/-0)
doc/en/whats-new/whats-new-in-2.5.txt (+17/-0)
Text conflict in bzrlib/config.py
To merge this branch: bzr merge lp:~jelmer/bzr/urllib-verifies-ssl-certs
Reviewer Review Type Date Requested Status
Martin Packman (community) Needs Fixing
Vincent Ladeuil Pending
Review via email: mp+86360@code.launchpad.net

This proposal supersedes a proposal from 2011-11-04.

This proposal has been superseded by a proposal from 2012-01-03.

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
6247. By Jelmer Vernooij

Add some tests for ssl.ca_certs config option.

6248. By Jelmer Vernooij

Add more tests for ssl.ca_certs option.

6249. By Jelmer Vernooij

add basic tests for match_hostname.

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

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
6250. By Jelmer Vernooij

Note new SSL certificate verification in whats-new.

6251. By Jelmer Vernooij

add notes on using -Ossl.cert_reqs=none.

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

> 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.

6252. By Jelmer Vernooij

Urgh - only warn if there actually isn't a ca certs file set.

6253. By Jelmer Vernooij

merge bzr.dev.

6254. By Jelmer Vernooij

Merge bzr.dev.

6255. By Jelmer Vernooij

merge bzr.dev.

Unmerged revisions

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