Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4ffb075

Browse files
committed
PEP 476: enable HTTPS certificate verification by default (#22417)
Patch by Alex Gaynor with some modifications by me.
1 parent 8cf7c1c commit 4ffb075

11 files changed

Lines changed: 116 additions & 69 deletions

File tree

Doc/library/http.client.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,6 @@ The module provides the following classes:
7171
:func:`ssl.create_default_context` select the system's trusted CA
7272
certificates for you.
7373

74-
The recommended way to connect to HTTPS hosts on the Internet is as
75-
follows::
76-
77-
context = ssl.create_default_context()
78-
h = client.HTTPSConnection('www.python.org', 443, context=context)
79-
8074
Please read :ref:`ssl-security` for more information on best practices.
8175

8276
.. note::
@@ -97,6 +91,12 @@ The module provides the following classes:
9791
The *strict* parameter was removed. HTTP 0.9-style "Simple Responses" are
9892
no longer supported.
9993

94+
.. versionchanged:: 3.4.3
95+
This class now performs all the necessary certificate and hostname checks
96+
by default. To revert to the previous, unverified, behavior
97+
:func:`ssl._create_unverified_context` can be passed to the *context*
98+
parameter.
99+
100100

101101
.. class:: HTTPResponse(sock, debuglevel=0, method=None, url=None)
102102

Doc/library/urllib.request.rst

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@ The :mod:`urllib.request` module defines the following functions:
6262
*cafile* and *capath* parameters are omitted. This will only work on
6363
some non-Windows platforms.
6464

65-
.. warning::
66-
If neither *cafile* nor *capath* is specified, and *cadefault* is ``False``,
67-
an HTTPS request will not do any verification of the server's
68-
certificate.
69-
7065
For http and https urls, this function returns a
7166
:class:`http.client.HTTPResponse` object which has the following
7267
:ref:`httpresponse-objects` methods.

Doc/library/xmlrpc.client.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ between conformable Python objects and XML on the wire.
2727
constructed data. If you need to parse untrusted or unauthenticated data see
2828
:ref:`xml-vulnerabilities`.
2929

30-
.. warning::
31-
32-
In the case of https URIs, :mod:`xmlrpc.client` does not do any verification
33-
of the server's certificate.
30+
.. versionchanged:: 3.4.3
3431

32+
For https URIs, :mod:`xmlrpc.client` now performs all the necessary
33+
certificate and hostname checks by default
3534

3635
.. class:: ServerProxy(uri, transport=None, encoding=None, verbose=False, \
3736
allow_none=False, use_datetime=False, \

Doc/whatsnew/3.4.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,3 +2504,32 @@ Changes in the C API
25042504
* The ``f_tstate`` (thread state) field of the :c:type:`PyFrameObject`
25052505
structure has been removed to fix a bug: see :issue:`14432` for the
25062506
rationale.
2507+
2508+
Changed in 3.4.3
2509+
================
2510+
2511+
.. _pep-476:
2512+
2513+
PEP 476: Enabling certificate verification by default for stdlib http clients
2514+
-----------------------------------------------------------------------------
2515+
2516+
:mod:`http.client` and modules which use it, such as :mod:`urllib.request` and
2517+
:mod:`xmlrpc.client`, will now verify that the server presents a certificate
2518+
which is signed by a CA in the platform trust store and whose hostname matches
2519+
the hostname being requested by default, significantly improving security for
2520+
many applications.
2521+
2522+
For applications which require the old previous behavior, they can pass an
2523+
alternate context::
2524+
2525+
import urllib.request
2526+
import ssl
2527+
2528+
# This disables all verification
2529+
context = ssl._create_unverified_context()
2530+
2531+
# This allows using a specific certificate for the host, which doesn't need
2532+
# to be in the trust store
2533+
context = ssl.create_default_context(cafile="/path/to/file.crt")
2534+
2535+
urllib.request.urlopen("https://invalid-cert", context=context)

Lib/http/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1203,7 +1203,7 @@ def __init__(self, host, port=None, key_file=None, cert_file=None,
12031203
self.key_file = key_file
12041204
self.cert_file = cert_file
12051205
if context is None:
1206-
context = ssl._create_stdlib_context()
1206+
context = ssl._create_default_https_context()
12071207
will_verify = context.verify_mode != ssl.CERT_NONE
12081208
if check_hostname is None:
12091209
check_hostname = will_verify

Lib/ssl.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,7 @@ def create_default_context(purpose=Purpose.SERVER_AUTH, *, cafile=None,
441441
context.load_default_certs(purpose)
442442
return context
443443

444-
445-
def _create_stdlib_context(protocol=PROTOCOL_SSLv23, *, cert_reqs=None,
444+
def _create_unverified_context(protocol=PROTOCOL_SSLv23, *, cert_reqs=None,
446445
check_hostname=False, purpose=Purpose.SERVER_AUTH,
447446
certfile=None, keyfile=None,
448447
cafile=None, capath=None, cadata=None):
@@ -480,6 +479,14 @@ def _create_stdlib_context(protocol=PROTOCOL_SSLv23, *, cert_reqs=None,
480479

481480
return context
482481

482+
# Used by http.client if no context is explicitly passed.
483+
_create_default_https_context = create_default_context
484+
485+
486+
# Backwards compatibility alias, even though it's not a public name.
487+
_create_stdlib_context = _create_unverified_context
488+
489+
483490
class SSLSocket(socket):
484491
"""This class implements a subtype of socket.socket that wraps
485492
the underlying OS socket in an SSL context when necessary, and

Lib/test/test_httplib.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -778,13 +778,36 @@ def _check_svn_python_org(self, resp):
778778
self.assertIn('Apache', server_string)
779779

780780
def test_networked(self):
781-
# Default settings: no cert verification is done
781+
# Default settings: requires a valid cert from a trusted CA
782+
import ssl
782783
support.requires('network')
783-
with support.transient_internet('svn.python.org'):
784-
h = client.HTTPSConnection('svn.python.org', 443)
784+
with support.transient_internet('self-signed.pythontest.net'):
785+
h = client.HTTPSConnection('self-signed.pythontest.net', 443)
786+
with self.assertRaises(ssl.SSLError) as exc_info:
787+
h.request('GET', '/')
788+
self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
789+
790+
def test_networked_noverification(self):
791+
# Switch off cert verification
792+
import ssl
793+
support.requires('network')
794+
with support.transient_internet('self-signed.pythontest.net'):
795+
context = ssl._create_unverified_context()
796+
h = client.HTTPSConnection('self-signed.pythontest.net', 443,
797+
context=context)
785798
h.request('GET', '/')
786799
resp = h.getresponse()
787-
self._check_svn_python_org(resp)
800+
self.assertIn('nginx', resp.getheader('server'))
801+
802+
def test_networked_trusted_by_default_cert(self):
803+
# Default settings: requires a valid cert from a trusted CA
804+
support.requires('network')
805+
with support.transient_internet('www.python.org'):
806+
h = client.HTTPSConnection('www.python.org', 443)
807+
h.request('GET', '/')
808+
resp = h.getresponse()
809+
content_type = resp.getheader('content-type')
810+
self.assertIn('text/html', content_type)
788811

789812
def test_networked_good_cert(self):
790813
# We feed a CA cert that validates the server's cert
@@ -803,13 +826,23 @@ def test_networked_bad_cert(self):
803826
# We feed a "CA" cert that is unrelated to the server's cert
804827
import ssl
805828
support.requires('network')
806-
with support.transient_internet('svn.python.org'):
829+
with support.transient_internet('self-signed.pythontest.net'):
807830
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
808831
context.verify_mode = ssl.CERT_REQUIRED
809832
context.load_verify_locations(CERT_localhost)
810-
h = client.HTTPSConnection('svn.python.org', 443, context=context)
811-
with self.assertRaises(ssl.SSLError):
833+
h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context)
834+
with self.assertRaises(ssl.SSLError) as exc_info:
812835
h.request('GET', '/')
836+
self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
837+
838+
def test_local_unknown_cert(self):
839+
# The custom cert isn't known to the default trust bundle
840+
import ssl
841+
server = self.make_server(CERT_localhost)
842+
h = client.HTTPSConnection('localhost', server.port)
843+
with self.assertRaises(ssl.SSLError) as exc_info:
844+
h.request('GET', '/')
845+
self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
813846

814847
def test_local_good_hostname(self):
815848
# The (valid) cert validates the HTTP hostname
@@ -822,7 +855,6 @@ def test_local_good_hostname(self):
822855
h.request('GET', '/nonexistent')
823856
resp = h.getresponse()
824857
self.assertEqual(resp.status, 404)
825-
del server
826858

827859
def test_local_bad_hostname(self):
828860
# The (valid) cert doesn't validate the HTTP hostname
@@ -845,7 +877,6 @@ def test_local_bad_hostname(self):
845877
h.request('GET', '/nonexistent')
846878
resp = h.getresponse()
847879
self.assertEqual(resp.status, 404)
848-
del server
849880

850881
@unittest.skipIf(not hasattr(client, 'HTTPSConnection'),
851882
'http.client.HTTPSConnection not available')

Lib/test/test_logging.py

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,36 +1631,6 @@ def tearDown(self):
16311631
class HTTPHandlerTest(BaseTest):
16321632
"""Test for HTTPHandler."""
16331633

1634-
PEMFILE = """-----BEGIN RSA PRIVATE KEY-----
1635-
MIICXQIBAAKBgQDGT4xS5r91rbLJQK2nUDenBhBG6qFk+bVOjuAGC/LSHlAoBnvG
1636-
zQG3agOG+e7c5z2XT8m2ktORLqG3E4mYmbxgyhDrzP6ei2Anc+pszmnxPoK3Puh5
1637-
aXV+XKt0bU0C1m2+ACmGGJ0t3P408art82nOxBw8ZHgIg9Dtp6xIUCyOqwIDAQAB
1638-
AoGBAJFTnFboaKh5eUrIzjmNrKsG44jEyy+vWvHN/FgSC4l103HxhmWiuL5Lv3f7
1639-
0tMp1tX7D6xvHwIG9VWvyKb/Cq9rJsDibmDVIOslnOWeQhG+XwJyitR0pq/KlJIB
1640-
5LjORcBw795oKWOAi6RcOb1ON59tysEFYhAGQO9k6VL621gRAkEA/Gb+YXULLpbs
1641-
piXN3q4zcHzeaVANo69tUZ6TjaQqMeTxE4tOYM0G0ZoSeHEdaP59AOZGKXXNGSQy
1642-
2z/MddcYGQJBAMkjLSYIpOLJY11ja8OwwswFG2hEzHe0cS9bzo++R/jc1bHA5R0Y
1643-
i6vA5iPi+wopPFvpytdBol7UuEBe5xZrxWMCQQCWxELRHiP2yWpEeLJ3gGDzoXMN
1644-
PydWjhRju7Bx3AzkTtf+D6lawz1+eGTuEss5i0JKBkMEwvwnN2s1ce+EuF4JAkBb
1645-
E96h1lAzkVW5OAfYOPY8RCPA90ZO/hoyg7PpSxR0ECuDrgERR8gXIeYUYfejBkEa
1646-
rab4CfRoVJKKM28Yq/xZAkBvuq670JRCwOgfUTdww7WpdOQBYPkzQccsKNCslQW8
1647-
/DyW6y06oQusSENUvynT6dr3LJxt/NgZPhZX2+k1eYDV
1648-
-----END RSA PRIVATE KEY-----
1649-
-----BEGIN CERTIFICATE-----
1650-
MIICGzCCAYSgAwIBAgIJAIq84a2Q/OvlMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
1651-
BAMTCWxvY2FsaG9zdDAeFw0xMTA1MjExMDIzMzNaFw03NTAzMjEwMzU1MTdaMBQx
1652-
EjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
1653-
xk+MUua/da2yyUCtp1A3pwYQRuqhZPm1To7gBgvy0h5QKAZ7xs0Bt2oDhvnu3Oc9
1654-
l0/JtpLTkS6htxOJmJm8YMoQ68z+notgJ3PqbM5p8T6Ctz7oeWl1flyrdG1NAtZt
1655-
vgAphhidLdz+NPGq7fNpzsQcPGR4CIPQ7aesSFAsjqsCAwEAAaN1MHMwHQYDVR0O
1656-
BBYEFLWaUPO6N7efGiuoS9i3DVYcUwn0MEQGA1UdIwQ9MDuAFLWaUPO6N7efGiuo
1657-
S9i3DVYcUwn0oRikFjAUMRIwEAYDVQQDEwlsb2NhbGhvc3SCCQCKvOGtkPzr5TAM
1658-
BgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAMK5whPjLNQK1Ivvk88oqJqq
1659-
4f889OwikGP0eUhOBhbFlsZs+jq5YZC2UzHz+evzKBlgAP1u4lP/cB85CnjvWqM+
1660-
1c/lywFHQ6HOdDeQ1L72tSYMrNOG4XNmLn0h7rx6GoTU7dcFRfseahBCq8mv0IDt
1661-
IRbTpvlHWPjsSvHz0ZOH
1662-
-----END CERTIFICATE-----"""
1663-
16641634
def setUp(self):
16651635
"""Set up an HTTP server to receive log messages, and a HTTPHandler
16661636
pointing to that server's address and port."""
@@ -1690,15 +1660,26 @@ def test_output(self):
16901660
if secure:
16911661
try:
16921662
import ssl
1693-
fd, fn = tempfile.mkstemp()
1694-
os.close(fd)
1695-
with open(fn, 'w') as f:
1696-
f.write(self.PEMFILE)
1697-
sslctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
1698-
sslctx.load_cert_chain(fn)
1699-
os.unlink(fn)
17001663
except ImportError:
17011664
sslctx = None
1665+
else:
1666+
here = os.path.dirname(__file__)
1667+
localhost_cert = os.path.join(here, "keycert.pem")
1668+
sslctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
1669+
sslctx.load_cert_chain(localhost_cert)
1670+
# Unfortunately, HTTPHandler doesn't allow us to change the
1671+
# SSLContext used by HTTPSConnection, so we have to
1672+
# monkeypatch. This can be cleaned up if issue 22788 is
1673+
# fixed.
1674+
old = ssl._create_default_https_context
1675+
def restore_handler():
1676+
ssl._create_default_https_context = old
1677+
self.addCleanup(restore_handler)
1678+
def hack_create_ctx():
1679+
ctx = old()
1680+
ctx.load_verify_locations(localhost_cert)
1681+
return ctx
1682+
ssl._create_default_https_context = hack_create_ctx
17021683
else:
17031684
sslctx = None
17041685
self.server = server = TestHTTPServer(addr, self.handle_request,

Lib/test/test_ssl.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2337,9 +2337,10 @@ def test_socketserver(self):
23372337
d1 = f.read()
23382338
d2 = ''
23392339
# now fetch the same data from the HTTPS server
2340-
url = 'https://%s:%d/%s' % (
2341-
HOST, server.port, os.path.split(CERTFILE)[1])
2342-
f = urllib.request.urlopen(url)
2340+
url = 'https://localhost:%d/%s' % (
2341+
server.port, os.path.split(CERTFILE)[1])
2342+
context = ssl.create_default_context(cafile=CERTFILE)
2343+
f = urllib.request.urlopen(url, context=context)
23432344
try:
23442345
dlen = f.info().get("content-length")
23452346
if dlen and (int(dlen) > 0):

Lib/test/test_urllib2_localnet.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,8 @@ def test_200_with_parameters(self):
545545

546546
def test_https(self):
547547
handler = self.start_https_server()
548-
data = self.urlopen("https://localhost:%s/bizarre" % handler.port)
548+
context = ssl.create_default_context(cafile=CERT_localhost)
549+
data = self.urlopen("https://localhost:%s/bizarre" % handler.port, context=context)
549550
self.assertEqual(data, b"we care a bit")
550551

551552
def test_https_with_cafile(self):
@@ -584,7 +585,8 @@ def cb_sni(ssl_sock, server_name, initial_context):
584585
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
585586
context.set_servername_callback(cb_sni)
586587
handler = self.start_https_server(context=context, certfile=CERT_localhost)
587-
self.urlopen("https://localhost:%s" % handler.port)
588+
context = ssl.create_default_context(cafile=CERT_localhost)
589+
self.urlopen("https://localhost:%s" % handler.port, context=context)
588590
self.assertEqual(sni_name, "localhost")
589591

590592
def test_sending_headers(self):

0 commit comments

Comments
 (0)