Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for TLS 1.3 to all HTTPSConnection implementations #1496

Merged
merged 37 commits into from Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f59e6b3
Add tests for specific TLS/SSL versions
sethmlarson Dec 6, 2018
417c333
Add change and update bindings
sethmlarson Dec 7, 2018
eaa6f38
SSLSocket.version() not available sometimes
sethmlarson Dec 7, 2018
20b5eb5
Add support for kTLSProtocolMaxSupported
sethmlarson Dec 7, 2018
ab1e014
Try setProtocolVersionMax again if error
sethmlarson Dec 7, 2018
fefc403
Get ctypes.c_uint.value for SSLSocket.version()
sethmlarson Dec 7, 2018
32de3a5
Opt-in TLS 1.3 on macOS 10.13
sethmlarson Dec 7, 2018
694b164
Update tornado to 5.1.1
sethmlarson Dec 7, 2018
9939524
Add documentation updates for TLSv1.3
sethmlarson Dec 10, 2018
493d78f
Add wbond/oscrypto license to contrib/securetransport
sethmlarson Dec 11, 2018
9fe5269
Remove all TLS 1.3 ciphersuites from DEFAULT_CIPHERS
sethmlarson Dec 11, 2018
522ed0c
Merge branch 'tls-1.3' of github.com:SethMichaelLarson/urllib3 into t…
sethmlarson Dec 11, 2018
b364e70
Experiment showing cipher list per protocol
sethmlarson Dec 17, 2018
209873a
Update test_https.py
sethmlarson Dec 17, 2018
490251b
Update test_https.py
sethmlarson Dec 17, 2018
93f1d3a
Update test_https.py
sethmlarson Dec 17, 2018
95e5935
Update changelog wording to exclude pyOpenSSL
sethmlarson Dec 18, 2018
1ae8674
minor rewording
sethmlarson Dec 18, 2018
4f6f74d
Add support for IPv6 in subjectAltName
sethmlarson Dec 19, 2018
a071345
Merge branch 'tls-1.3' of github.com:SethMichaelLarson/urllib3 into t…
sethmlarson Dec 19, 2018
a18f623
Don't use OP_ALL
sethmlarson Dec 19, 2018
a97cefe
Update CHANGES.rst
sethmlarson Dec 19, 2018
ca52ca5
No PROTOCOL_TLSv1_3
sethmlarson Dec 21, 2018
7e4e485
Remove DSS, rearrange SecureTransport ciphers
sethmlarson Dec 26, 2018
5745dfb
Use ECDSA before RSA with ECDHE
sethmlarson Dec 26, 2018
2bc2742
ReviReorder ciphers
sethmlarson Dec 27, 2018
6a4d3dc
ECDHE
sethmlarson Dec 27, 2018
9fc3c5a
Update test_https.py
sethmlarson Dec 28, 2018
423df77
Turns out we don't need version detection
sethmlarson Dec 28, 2018
4244f75
Reorder per Hyneks post and favor ephemeral
sethmlarson Jan 30, 2019
3817503
Merge branch 'master' into tls-1.3
sethmlarson Jan 30, 2019
18001cd
Refactor HTTPS unit tests
sethmlarson Feb 19, 2019
eac9b3a
Merge branch 'tls-1.3' of ssh://github.com/sethmlarson/urllib3 into t…
sethmlarson Feb 19, 2019
3b9c529
Fix up tests
sethmlarson Feb 19, 2019
ccb3737
Test locking pytest-httpbin
sethmlarson Feb 27, 2019
9e78231
Update requests.sh
sethmlarson Feb 27, 2019
15c3af7
remove whitespace
sethmlarson Feb 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -6,6 +6,8 @@ dev (master)

* Implemented a more efficient ``HTTPResponse.__iter__()`` method (Issue #1483)

* Added support for TLSv1.3 support for all ``HTTPSConnection`` implementations. (PR #1496)

* ... [Short description of non-trivial change.] (Issue #)


Expand Down
12 changes: 9 additions & 3 deletions dummyserver/testcase.py
Expand Up @@ -115,13 +115,16 @@ class HTTPDummyServerTestCase(unittest.TestCase):
scheme = 'http'
host = 'localhost'
host_alt = '127.0.0.1' # Some tests need two hosts
certs = DEFAULT_CERTS

@classmethod
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Just for my own edification) what's the benefit of doing a classmethod wrapper here over a property?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be mistaken because I've never tried it but properties aren't available to the class only the instance? We need it for _start_server() which is also a classmethod.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious why the change was necessary, I assumed the old certs would be available to the class too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a function here because we're making a copy of DEFAULT_CERTS and changing the ssl_version key per-test for the TLS version tests. If there's an alternate way to achieve this I could change this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you're saying before we'd have to do something like...

tlsv1_certs = DEFAULT_CERTS.copy()
tlsv1_certs['ssl_version'] = ssl.PROTOCOL_TLSv1

class TestHTTPS_TLSv1(TestHTTPS_TLSVersion):
    tls_protocol_name = 'TLSv1'
    certs = tlsv1_certs

Instead of...

class TestHTTPS_TLSv1(TestHTTPS_TLSVersion):
    tls_protocol_name = 'TLSv1'

     @classmethod
    def certs(cls):
        certs = DEFAULT_CERTS.copy()
        certs['ssl_version'] = ssl.PROTOCOL_TLSv1
        return certs

?

Not obvious to me what the advantage of the latter one is, but I don't have strong feelings here. :)

def certs(cls):
return DEFAULT_CERTS

@classmethod
def _start_server(cls):
cls.io_loop = ioloop.IOLoop.current()
app = web.Application([(r".*", TestingApp)])
cls.server, cls.port = run_tornado_app(app, cls.io_loop, cls.certs,
cls.server, cls.port = run_tornado_app(app, cls.io_loop, cls.certs(),
cls.scheme, cls.host)
cls.server_thread = run_loop_in_thread(cls.io_loop)

Expand All @@ -143,7 +146,10 @@ def tearDownClass(cls):
class HTTPSDummyServerTestCase(HTTPDummyServerTestCase):
scheme = 'https'
host = 'localhost'
certs = DEFAULT_CERTS

@classmethod
def certs(cls):
return DEFAULT_CERTS


@pytest.mark.skipif(not HAS_IPV6, reason='IPv6 not available')
Expand Down
2 changes: 2 additions & 0 deletions src/urllib3/contrib/_securetransport/bindings.py
Expand Up @@ -516,6 +516,8 @@ class SecurityConst(object):
kTLSProtocol1 = 4
kTLSProtocol11 = 7
kTLSProtocol12 = 8
kTLSProtocol13 = 10
kTLSProtocolMaxSupported = 999

kSSLClientSide = 1
kSSLStreamType = 0
Expand Down
3 changes: 3 additions & 0 deletions src/urllib3/contrib/pyopenssl.py
Expand Up @@ -360,6 +360,9 @@ def getpeercert(self, binary_form=False):
'subjectAltName': get_subj_alt_name(x509)
}

def version(self):
return self.connection.get_protocol_version_name()

def _reuse(self):
self._makefile_refs += 1

Expand Down
25 changes: 24 additions & 1 deletion src/urllib3/contrib/securetransport.py
Expand Up @@ -124,7 +124,7 @@
# Basically this is simple: for PROTOCOL_SSLv23 we turn it into a low of
# TLSv1 and a high of TLSv1.2. For everything else, we pin to that version.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the high is now TLSv1.3

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update this. Also is kTLSProtocol13 not available? I'm not sure when it was added to SecureTransport but I'm getting Illegal Parameter errors for using it. :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://developer.apple.com/documentation/security/sslprotocol/ktlsprotocol13?language=objc mentions macOS 10.13+, which is what Travis uses. That's all I know, sorry!

_protocol_to_min_max = {
ssl.PROTOCOL_SSLv23: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12),
ssl.PROTOCOL_SSLv23: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocolMaxSupported),
}

if hasattr(ssl, "PROTOCOL_SSLv2"):
Expand All @@ -147,6 +147,10 @@
_protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = (
SecurityConst.kTLSProtocol12, SecurityConst.kTLSProtocol12
)
if hasattr(ssl, "PROTOCOL_TLSv1_3"):
_protocol_to_min_max[ssl.PROTOCOL_TLSv1_3] = (
SecurityConst.kTLSProtocol13, SecurityConst.kTLSProtocol13
)
if hasattr(ssl, "PROTOCOL_TLS"):
_protocol_to_min_max[ssl.PROTOCOL_TLS] = _protocol_to_min_max[ssl.PROTOCOL_SSLv23]

Expand Down Expand Up @@ -667,6 +671,25 @@ def getpeercert(self, binary_form=False):

return der_bytes

def version(self):
protocol = Security.SSLProtocol()
result = Security.SSLGetNegotiatedProtocolVersion(self.context, ctypes.byref(protocol))
_assert_no_error(result)
if protocol == SecurityConst.kTLSProtocol13:
return 'TLSv1.3'
elif protocol == SecurityConst.kTLSProtocol12:
return 'TLSv1.2'
elif protocol == SecurityConst.kTLSProtocol11:
return 'TLSv1.1'
elif protocol == SecurityConst.kTLSProtocol1:
return 'TLSv1'
elif protocol == SecurityConst.kSSLProtocol3:
return 'SSLv3'
elif protocol == SecurityConst.kSSLProtocol2:
return 'SSLv2'
else:
raise ssl.SSLError('Unknown TLS version: %r' % protocol)

def _reuse(self):
self._makefile_refs += 1

Expand Down
6 changes: 5 additions & 1 deletion test/contrib/test_pyopenssl.py
Expand Up @@ -31,7 +31,11 @@ def teardown_module():
pass


from ..with_dummyserver.test_https import TestHTTPS, TestHTTPS_TLSv1 # noqa: F401
from ..with_dummyserver.test_https import ( # noqa: F401
TestHTTPS, TestHTTPS_TLSv1, TestHTTPS_TLSv1_1,
TestHTTPS_TLSv1_2, TestHTTPS_TLSv1_3, TestHTTPS_IPSAN,
TestHTTPS_IPv6Addr, TestHTTPS_NoSAN
)
from ..with_dummyserver.test_socketlevel import ( # noqa: F401
TestSNI, TestSocketClosing, TestClientCerts
)
Expand Down
5 changes: 4 additions & 1 deletion test/contrib/test_securetransport.py
Expand Up @@ -27,7 +27,10 @@ def teardown_module():
pass


from ..with_dummyserver.test_https import TestHTTPS, TestHTTPS_TLSv1 # noqa: F401
from ..with_dummyserver.test_https import ( # noqa: F401
TestHTTPS, TestHTTPS_TLSv1, TestHTTPS_TLSv1_1,
TestHTTPS_TLSv1_2, TestHTTPS_TLSv1_3
)
from ..with_dummyserver.test_socketlevel import ( # noqa: F401
TestSNI, TestSocketClosing, TestClientCerts
)
Expand Down
95 changes: 70 additions & 25 deletions test/with_dummyserver/test_https.py
Expand Up @@ -72,11 +72,6 @@ def test_dotted_fqdn(self):
r = pool.request('GET', '/')
self.assertEqual(r.status, 200, r.data)

def test_set_ssl_version_to_tlsv1(self):
self._pool.ssl_version = ssl.PROTOCOL_TLSv1
r = self._pool.request('GET', '/')
self.assertEqual(r.status, 200, r.data)

def test_client_intermediate(self):
client_cert, client_key = (
DEFAULT_CLIENT_CERTS['certfile'],
Expand Down Expand Up @@ -559,30 +554,76 @@ def _request_without_resource_warnings(self, method, url):
return [x for x in w if not isinstance(x.message, ResourceWarning)]


class TestHTTPS_TLSv1(HTTPSDummyServerTestCase):
certs = DEFAULT_CERTS.copy()
certs['ssl_version'] = ssl.PROTOCOL_TLSv1
class TestHTTPS_TLSVersion(TestHTTPS):
tls_protocol_name = None

def setUp(self):
self._pool = HTTPSConnectionPool(self.host, self.port)
self.addCleanup(self._pool.close)
@classmethod
def certs(cls):
return pytest.skip('Don\'t run parent version.')

def test_discards_connection_on_sslerror(self):
self._pool.cert_reqs = 'CERT_REQUIRED'
with self.assertRaises(MaxRetryError) as cm:
self._pool.request('GET', '/', retries=0)
self.assertIsInstance(cm.exception.reason, SSLError)
self._pool.ca_certs = DEFAULT_CA
self._pool.request('GET', '/')
def test_set_ssl_version_to_tls_version(self):
self._pool.ssl_version = self.certs()['ssl_version']
r = self._pool.request('GET', '/')
self.assertEqual(r.status, 200, r.data)

def test_set_cert_default_cert_required(self):
conn = VerifiedHTTPSConnection(self.host, self.port)
conn.set_cert(ca_certs=DEFAULT_CA)
self.assertEqual(conn.cert_reqs, 'CERT_REQUIRED')
def test_tls_protocol_name_of_socket(self):
conn = self._pool._get_conn()
conn.connect()

if not hasattr(conn.sock, 'version'):
pytest.skip('SSLSocket.version() not available')

self.assertEqual(conn.sock.version(), self.tls_protocol_name)


@pytest.mark.skipif(not hasattr(ssl, "PROTOCOL_TLSv1"), reason="Requires TLSv1 support")
class TestHTTPS_TLSv1(TestHTTPS_TLSVersion):
tls_protocol_name = 'TLSv1'

@classmethod
def certs(cls):
certs = DEFAULT_CERTS.copy()
certs['ssl_version'] = ssl.PROTOCOL_TLSv1
return certs


@pytest.mark.skipif(not hasattr(ssl, "PROTOCOL_TLSv1_1"), reason="Requires TLSv1.1 support")
class TestHTTPS_TLSv1_1(TestHTTPS_TLSVersion):
tls_protocol_name = 'TLSv1.1'

@classmethod
def certs(cls):
certs = DEFAULT_CERTS.copy()
certs['ssl_version'] = ssl.PROTOCOL_TLSv1_1
return certs


@pytest.mark.skipif(not hasattr(ssl, "PROTOCOL_TLSv1_2"), reason="Requires TLSv1.2 support")
class TestHTTPS_TLSv1_2(TestHTTPS_TLSVersion):
tls_protocol_name = 'TLSv1.2'

@classmethod
def certs(cls):
certs = DEFAULT_CERTS.copy()
certs['ssl_version'] = ssl.PROTOCOL_TLSv1_2
return certs


@pytest.mark.skipif(not hasattr(ssl, "PROTOCOL_TLSv1_3"), reason="Requires TLSv1.3 support")
class TestHTTPS_TLSv1_3(TestHTTPS_TLSVersion):
tls_protocol_name = 'TLSv1.3'

@classmethod
def certs(cls):
certs = DEFAULT_CERTS.copy()
certs['ssl_version'] = ssl.PROTOCOL_TLSv1_3
return certs


class TestHTTPS_NoSAN(HTTPSDummyServerTestCase):
certs = NO_SAN_CERTS
@classmethod
def certs(cls):
return NO_SAN_CERTS

def test_warning_for_certs_without_a_san(self):
"""Ensure that a warning is raised when the cert from the server has
Expand All @@ -598,7 +639,9 @@ def test_warning_for_certs_without_a_san(self):


class TestHTTPS_IPSAN(HTTPSDummyServerTestCase):
certs = IP_SAN_CERTS
@classmethod
def certs(cls):
return IP_SAN_CERTS

def test_can_validate_ip_san(self):
"""Ensure that urllib3 can validate SANs with IP addresses in them."""
Expand All @@ -616,7 +659,9 @@ def test_can_validate_ip_san(self):


class TestHTTPS_IPv6Addr(IPV6HTTPSDummyServerTestCase):
certs = IPV6_ADDR_CERTS
@classmethod
def certs(cls):
return IPV6_ADDR_CERTS

@pytest.mark.skipif(not HAS_IPV6, reason='Only runs on IPv6 systems')
def test_strip_square_brackets_before_validating(self):
Expand Down
2 changes: 1 addition & 1 deletion test/with_dummyserver/test_socketlevel.py
Expand Up @@ -626,7 +626,7 @@ def socket_handler(listener):
# First request should fail.
response = pool.urlopen('GET', '/', retries=0,
preload_content=False,
timeout=Timeout(connect=1, read=0.001))
timeout=Timeout(connect=1, read=0.1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😢

try:
self.assertRaises(ReadTimeoutError, response.read)
finally:
Expand Down