diff --git a/CHANGES.rst b/CHANGES.rst index adc55a68e2..29c2676ffc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,14 @@ dev (master) ``brotlipy`` package is installed which can be requested with ``urllib3[brotli]`` extra. (Pull #1532) +* Add TLSv1.3 support to CPython, pyOpenSSL, and SecureTransport ``SSLContext`` + implementations. (Pull #1496) + +* Drop ciphers using DSS key exchange from default TLS cipher suites. + Improve default ciphers when using SecureTransport. (Pull #1496) + +* Add support for IPv6 addresses in subjectAltName section of certificates. (Issue #1269) + * ... [Short description of non-trivial change.] (Issue #) diff --git a/_travis/downstream/requests.sh b/_travis/downstream/requests.sh index a3a32ac736..3c5f4551db 100755 --- a/_travis/downstream/requests.sh +++ b/_travis/downstream/requests.sh @@ -4,11 +4,14 @@ set -exo pipefail case "${1}" in install) - git clone --depth 1 https://github.com/requests/requests + git clone --depth 1 https://github.com/kennethreitz/requests cd requests git rev-parse HEAD python -m pip install --upgrade pipenv pipenv install --dev --skip-lock + + # See: kennethreitz/requests/5004 + python -m pip install pytest-httpbin==0.3.0 ;; run) cd requests diff --git a/_travis/install.sh b/_travis/install.sh index 1999c7e6d0..6bdfa58f69 100755 --- a/_travis/install.sh +++ b/_travis/install.sh @@ -20,6 +20,9 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then # The pip in older MacPython releases doesn't support a new enough TLS curl https://bootstrap.pypa.io/get-pip.py | sudo $PYTHON_EXE $PYTHON_EXE -m pip install virtualenv + + # Enable TLS 1.3 on macOS + sudo defaults write /Library/Preferences/com.apple.networkd tcp_connect_enable_tls13 1 else python -m pip install virtualenv fi diff --git a/dev-requirements.txt b/dev-requirements.txt index 264e844f60..2e8d258f0d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,7 +2,7 @@ mock==2.0.0 coverage~=4.5 tox==2.9.1 wheel==0.30.0 -tornado==5.0.2 +tornado==5.1.1 PySocks==1.6.8 pkginfo==1.4.2 pytest-timeout==1.3.1 diff --git a/dummyserver/certs/server.ipv6_san.crt b/dummyserver/certs/server.ipv6_san.crt new file mode 100644 index 0000000000..64acace329 --- /dev/null +++ b/dummyserver/certs/server.ipv6_san.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICfTCCAeagAwIBAgIJAPcpn3/M5+piMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTgxMjE5MDUyMjUyWhcNNDgxMjE4MDUyMjUyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDXe3FqmCWvP8XPxqtT+0bfL1Tvzvebi46k0WIcUV8bP3vyYiSRXG9ALmyzZH4G +HY9UVs4OEDkCMDOBSezB0y9ai/9doTNcaictdEBu8nfdXKoTtzrn+VX4UPrkH5hm +7NQ1fTQuj1MR7yBCmYqN3Q2Q+Efuujyx0FwBzAuy1aKYuwIDAQABo3UwczAdBgNV +HQ4EFgQUG+dK5Uos08QUwAWofDb3a8YcYlIwHwYDVR0jBBgwFoAUG+dK5Uos08QU +wAWofDb3a8YcYlIwDwYDVR0TAQH/BAUwAwEB/zAgBgNVHREEGTAXggM6OjGHEAAA +AAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADgYEAjT767TDq6q4lOextf3tZ +BjeuYDUy7bb1fDBAN5rBT1ywr7r0JE6/KOnsZx4jbevx3MllxNpx0gOM2bgwJlnG +8tgwRB6pxDyln01WBj9b5ymK60jdkw7gg3yYpqEs5/VBQidFO3BmDqf5cGO8PU7p +0VWdfJBP2UbwblNXdImI1zk= +-----END CERTIFICATE----- diff --git a/dummyserver/server.py b/dummyserver/server.py index 8f6e5fb508..8f0794f91d 100755 --- a/dummyserver/server.py +++ b/dummyserver/server.py @@ -60,11 +60,16 @@ 'certfile': os.path.join(CERTS_PATH, 'server.ipv6addr.crt'), 'keyfile': os.path.join(CERTS_PATH, 'server.ipv6addr.key'), } +IPV6_SAN_CERTS = { + 'certfile': os.path.join(CERTS_PATH, 'server.ipv6_san.crt'), + 'keyfile': DEFAULT_CERTS['keyfile'] +} DEFAULT_CA = os.path.join(CERTS_PATH, 'cacert.pem') DEFAULT_CA_BAD = os.path.join(CERTS_PATH, 'client_bad.pem') NO_SAN_CA = os.path.join(CERTS_PATH, 'cacert.no_san.pem') DEFAULT_CA_DIR = os.path.join(CERTS_PATH, 'ca_path_test') IPV6_ADDR_CA = os.path.join(CERTS_PATH, 'server.ipv6addr.crt') +IPV6_SAN_CA = os.path.join(CERTS_PATH, 'server.ipv6_san.crt') COMBINED_CERT_AND_KEY = os.path.join(CERTS_PATH, 'server.combined.pem') diff --git a/src/urllib3/contrib/_securetransport/bindings.py b/src/urllib3/contrib/_securetransport/bindings.py index bcf41c02b2..be34215359 100644 --- a/src/urllib3/contrib/_securetransport/bindings.py +++ b/src/urllib3/contrib/_securetransport/bindings.py @@ -516,6 +516,8 @@ class SecurityConst(object): kTLSProtocol1 = 4 kTLSProtocol11 = 7 kTLSProtocol12 = 8 + kTLSProtocol13 = 10 + kTLSProtocolMaxSupported = 999 kSSLClientSide = 1 kSSLStreamType = 0 @@ -558,30 +560,27 @@ class SecurityConst(object): errSecInvalidTrustSettings = -25262 # Cipher suites. We only pick the ones our default cipher string allows. + # Source: https://developer.apple.com/documentation/security/1550981-ssl_cipher_suite_values TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xC030 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F - TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 = 0x00A3 + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA9 + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA8 TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = 0x009F - TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 = 0x00A2 TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = 0x009E TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xC024 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xC028 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xC00A TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 0xC014 TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 0x006B - TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 = 0x006A TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x0039 - TLS_DHE_DSS_WITH_AES_256_CBC_SHA = 0x0038 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xC023 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xC027 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xC009 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xC013 TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 0x0067 - TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 = 0x0040 TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x0033 - TLS_DHE_DSS_WITH_AES_128_CBC_SHA = 0x0032 TLS_RSA_WITH_AES_256_GCM_SHA384 = 0x009D TLS_RSA_WITH_AES_128_GCM_SHA256 = 0x009C TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x003D @@ -590,4 +589,5 @@ class SecurityConst(object): TLS_RSA_WITH_AES_128_CBC_SHA = 0x002F TLS_AES_128_GCM_SHA256 = 0x1301 TLS_AES_256_GCM_SHA384 = 0x1302 - TLS_CHACHA20_POLY1305_SHA256 = 0x1303 + TLS_AES_128_CCM_8_SHA256 = 0x1305 + TLS_AES_128_CCM_SHA256 = 0x1304 diff --git a/src/urllib3/contrib/pyopenssl.py b/src/urllib3/contrib/pyopenssl.py index 9a5b029301..821c174fdc 100644 --- a/src/urllib3/contrib/pyopenssl.py +++ b/src/urllib3/contrib/pyopenssl.py @@ -70,6 +70,7 @@ class UnsupportedExtension(Exception): from .. import util + __all__ = ['inject_into_urllib3', 'extract_from_urllib3'] # SNI always works. @@ -77,20 +78,19 @@ class UnsupportedExtension(Exception): # Map from urllib3 to PyOpenSSL compatible parameter-values. _openssl_versions = { - ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD, + util.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, } +if hasattr(ssl, 'PROTOCOL_SSLv3') and hasattr(OpenSSL.SSL, 'SSLv3_METHOD'): + _openssl_versions[ssl.PROTOCOL_SSLv3] = OpenSSL.SSL.SSLv3_METHOD + if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'): _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD -try: - _openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD}) -except AttributeError: - pass _stdlib_to_openssl_verify = { ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, @@ -186,6 +186,10 @@ def idna_encode(name): except idna.core.IDNAError: return None + # Don't send IPv6 addresses through the IDNA encoder. + if ':' in name: + return name + name = idna_encode(name) if name is None: return None @@ -288,6 +292,10 @@ def recv(self, *args, **kwargs): raise timeout('The read operation timed out') else: return self.recv(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError("read error: %r" % e) else: return data @@ -310,6 +318,10 @@ def recv_into(self, *args, **kwargs): else: return self.recv_into(*args, **kwargs) + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError("read error: %r" % e) + def settimeout(self, timeout): return self.socket.settimeout(timeout) @@ -362,6 +374,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 diff --git a/src/urllib3/contrib/securetransport.py b/src/urllib3/contrib/securetransport.py index 109eb9a16c..4dc4848416 100644 --- a/src/urllib3/contrib/securetransport.py +++ b/src/urllib3/contrib/securetransport.py @@ -23,6 +23,31 @@ urllib3.contrib.securetransport.inject_into_urllib3() Happy TLSing! + +This code is a bastardised version of the code found in Will Bond's oscrypto +library. An enormous debt is owed to him for blazing this trail for us. For +that reason, this code should be considered to be covered both by urllib3's +license and by oscrypto's: + + Copyright (c) 2015-2016 Will Bond + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. """ from __future__ import absolute_import @@ -86,35 +111,32 @@ # individual cipher suites. We need to do this because this is how # SecureTransport wants them. CIPHER_SUITES = [ - SecurityConst.TLS_AES_256_GCM_SHA384, - SecurityConst.TLS_CHACHA20_POLY1305_SHA256, - SecurityConst.TLS_AES_128_GCM_SHA256, SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - SecurityConst.TLS_DHE_DSS_WITH_AES_256_GCM_SHA384, + SecurityConst.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + SecurityConst.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, SecurityConst.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, - SecurityConst.TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, SecurityConst.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, - SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, - SecurityConst.TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, - SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA, - SecurityConst.TLS_DHE_DSS_WITH_AES_256_CBC_SHA, SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, + SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, + SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA, SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, - SecurityConst.TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, - SecurityConst.TLS_DHE_DSS_WITH_AES_128_CBC_SHA, + SecurityConst.TLS_AES_256_GCM_SHA384, + SecurityConst.TLS_AES_128_GCM_SHA256, SecurityConst.TLS_RSA_WITH_AES_256_GCM_SHA384, SecurityConst.TLS_RSA_WITH_AES_128_GCM_SHA256, + SecurityConst.TLS_AES_128_CCM_8_SHA256, + SecurityConst.TLS_AES_128_CCM_SHA256, SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA256, SecurityConst.TLS_RSA_WITH_AES_128_CBC_SHA256, SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA, @@ -122,9 +144,10 @@ ] # 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. +# TLSv1 and a high of TLSv1.3. For everything else, we pin to that version. +# TLSv1 to 1.2 are supported on macOS 10.8+ and TLSv1.3 is macOS 10.13+ _protocol_to_min_max = { - ssl.PROTOCOL_SSLv23: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12), + util.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocolMaxSupported), } if hasattr(ssl, "PROTOCOL_SSLv2"): @@ -147,8 +170,6 @@ _protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = ( SecurityConst.kTLSProtocol12, SecurityConst.kTLSProtocol12 ) -if hasattr(ssl, "PROTOCOL_TLS"): - _protocol_to_min_max[ssl.PROTOCOL_TLS] = _protocol_to_min_max[ssl.PROTOCOL_SSLv23] def inject_into_urllib3(): @@ -460,7 +481,14 @@ def handshake(self, # Set the minimum and maximum TLS versions. result = Security.SSLSetProtocolVersionMin(self.context, min_version) _assert_no_error(result) + + # TLS 1.3 isn't necessarily enabled by the OS + # so we have to detect when we error out and try + # setting TLS 1.3 if it's allowed. kTLSProtocolMaxSupported + # was added in macOS 10.13 along with kTLSProtocol13. result = Security.SSLSetProtocolVersionMax(self.context, max_version) + if result != 0 and max_version == SecurityConst.kTLSProtocolMaxSupported: + result = Security.SSLSetProtocolVersionMax(self.context, SecurityConst.kTLSProtocol12) _assert_no_error(result) # If there's a trust DB, we need to use it. We do that by telling @@ -669,6 +697,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.value == SecurityConst.kTLSProtocol13: + return 'TLSv1.3' + elif protocol.value == SecurityConst.kTLSProtocol12: + return 'TLSv1.2' + elif protocol.value == SecurityConst.kTLSProtocol11: + return 'TLSv1.1' + elif protocol.value == SecurityConst.kTLSProtocol1: + return 'TLSv1' + elif protocol.value == SecurityConst.kSSLProtocol3: + return 'SSLv3' + elif protocol.value == SecurityConst.kSSLProtocol2: + return 'SSLv2' + else: + raise ssl.SSLError('Unknown TLS version: %r' % protocol) + def _reuse(self): self._makefile_refs += 1 diff --git a/src/urllib3/util/__init__.py b/src/urllib3/util/__init__.py index 2f2770b622..2914bb468b 100644 --- a/src/urllib3/util/__init__.py +++ b/src/urllib3/util/__init__.py @@ -12,6 +12,7 @@ resolve_cert_reqs, resolve_ssl_version, ssl_wrap_socket, + PROTOCOL_TLS, ) from .timeout import ( current_time, @@ -35,6 +36,7 @@ 'IS_PYOPENSSL', 'IS_SECURETRANSPORT', 'SSLContext', + 'PROTOCOL_TLS', 'Retry', 'Timeout', 'Url', diff --git a/src/urllib3/util/ssl_.py b/src/urllib3/util/ssl_.py index 6fb1d0e929..0327a923ad 100644 --- a/src/urllib3/util/ssl_.py +++ b/src/urllib3/util/ssl_.py @@ -54,11 +54,21 @@ def _const_compare_digest_backport(a, b): try: # Test for SSL features import ssl - from ssl import wrap_socket, CERT_REQUIRED, PROTOCOL_SSLv23 + from ssl import wrap_socket, CERT_REQUIRED from ssl import HAS_SNI # Has SNI? except ImportError: pass +try: # Platform-specific: Python 3.6 + from ssl import PROTOCOL_TLS + PROTOCOL_SSLv23 = PROTOCOL_TLS +except ImportError: + try: + from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS + PROTOCOL_SSLv23 = PROTOCOL_TLS + except ImportError: + PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 + try: from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION @@ -75,30 +85,30 @@ def _const_compare_digest_backport(a, b): # - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ # # The general intent is: -# - Prefer TLS 1.3 cipher suites # - prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE), # - prefer ECDHE over DHE for better performance, # - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and # security, # - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common, -# - disable NULL authentication, MD5 MACs and DSS for security reasons. +# - disable NULL authentication, MD5 MACs, DSS, and other +# insecure ciphers for security reasons. +# - NOTE: TLS 1.3 cipher suites are managed through a different interface +# not exposed by CPython (yet!) and are enabled by default if they're available. DEFAULT_CIPHERS = ':'.join([ - 'TLS13-AES-256-GCM-SHA384', - 'TLS13-CHACHA20-POLY1305-SHA256', - 'TLS13-AES-128-GCM-SHA256', + 'ECDHE+AESGCM', + 'ECDHE+CHACHA20', + 'DHE+AESGCM', + 'DHE+CHACHA20', 'ECDH+AESGCM', - 'ECDH+CHACHA20', 'DH+AESGCM', - 'DH+CHACHA20', - 'ECDH+AES256', - 'DH+AES256', - 'ECDH+AES128', + 'ECDH+AES', 'DH+AES', 'RSA+AESGCM', 'RSA+AES', '!aNULL', '!eNULL', '!MD5', + '!DSS', ]) try: @@ -205,7 +215,7 @@ def resolve_ssl_version(candidate): like resolve_cert_reqs """ if candidate is None: - return PROTOCOL_SSLv23 + return PROTOCOL_TLS if isinstance(candidate, str): res = getattr(ssl, candidate, None) @@ -251,7 +261,7 @@ def create_urllib3_context(ssl_version=None, cert_reqs=None, Constructed SSLContext object with specified options :rtype: SSLContext """ - context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23) + context = SSLContext(ssl_version or PROTOCOL_TLS) context.set_ciphers(ciphers or DEFAULT_CIPHERS) diff --git a/test/__init__.py b/test/__init__.py index b5ead0d47a..ed4e78c77d 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -177,6 +177,28 @@ def wrapper(*args, **kwargs): return wrapper +def requiresTLSv1(): + """Test requires TLSv1 available""" + return pytest.mark.skipif(not hasattr(ssl, "PROTOCOL_TLSv1"), reason="Test requires TLSv1") + + +def requiresTLSv1_1(): + """Test requires TLSv1.1 available""" + return pytest.mark.skipif(not hasattr(ssl, "PROTOCOL_TLSv1_1"), reason="Test requires TLSv1.1") + + +def requiresTLSv1_2(): + """Test requires TLSv1.2 available""" + return pytest.mark.skipif(not hasattr(ssl, "PROTOCOL_TLSv1_2"), reason="Test requires TLSv1.2") + + +def requiresTLSv1_3(): + """Test requires TLSv1.3 available""" + return pytest.mark.skipif( + not getattr(ssl, "HAS_TLSv1_3", False), reason="Test requires TLSv1.3" + ) + + class _ListHandler(logging.Handler): def __init__(self): super(_ListHandler, self).__init__() diff --git a/test/contrib/test_pyopenssl.py b/test/contrib/test_pyopenssl.py index 21cde45a03..31fb299a6d 100644 --- a/test/contrib/test_pyopenssl.py +++ b/test/contrib/test_pyopenssl.py @@ -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, TestHTTPS_IPV6SAN +) from ..with_dummyserver.test_socketlevel import ( # noqa: F401 TestSNI, TestSocketClosing, TestClientCerts ) diff --git a/test/contrib/test_securetransport.py b/test/contrib/test_securetransport.py index 0a2c2a8cac..623b5ef46d 100644 --- a/test/contrib/test_securetransport.py +++ b/test/contrib/test_securetransport.py @@ -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 ) diff --git a/test/with_dummyserver/test_https.py b/test/with_dummyserver/test_https.py index 648b5561cc..eafd40b0b6 100644 --- a/test/with_dummyserver/test_https.py +++ b/test/with_dummyserver/test_https.py @@ -17,7 +17,8 @@ DEFAULT_CLIENT_NO_INTERMEDIATE_CERTS, NO_SAN_CERTS, NO_SAN_CA, DEFAULT_CA_DIR, IPV6_ADDR_CERTS, IPV6_ADDR_CA, HAS_IPV6, - IP_SAN_CERTS, PASSWORD_CLIENT_KEYFILE) + IP_SAN_CERTS, IPV6_SAN_CERTS, IPV6_SAN_CA, + PASSWORD_CLIENT_KEYFILE) from test import ( onlyPy279OrNewer, @@ -26,6 +27,10 @@ requires_network, requires_ssl_context_keyfile_password, fails_on_travis_gce, + requiresTLSv1, + requiresTLSv1_1, + requiresTLSv1_2, + requiresTLSv1_3, TARPIT_HOST, ) from urllib3 import HTTPSConnectionPool @@ -57,7 +62,19 @@ log.addHandler(logging.StreamHandler(sys.stdout)) +TLSv1_CERTS = DEFAULT_CERTS.copy() +TLSv1_CERTS["ssl_version"] = getattr(ssl, "PROTOCOL_TLSv1", None) + +TLSv1_1_CERTS = DEFAULT_CERTS.copy() +TLSv1_1_CERTS["ssl_version"] = getattr(ssl, "PROTOCOL_TLSv1_1", None) + +TLSv1_2_CERTS = DEFAULT_CERTS.copy() +TLSv1_2_CERTS["ssl_version"] = getattr(ssl, "PROTOCOL_TLSv1_2", None) + + class TestHTTPS(HTTPSDummyServerTestCase): + tls_protocol_name = None + def setUp(self): self._pool = HTTPSConnectionPool(self.host, self.port, ca_certs=DEFAULT_CA) self.addCleanup(self._pool.close) @@ -72,11 +89,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'], @@ -591,28 +603,54 @@ def _request_without_resource_warnings(self, method, url): return [x for x in w if not isinstance(x.message, ResourceWarning)] + def test_set_ssl_version_to_tls_version(self): + if self.tls_protocol_name is None: + pytest.skip("Skipping base test class") -class TestHTTPS_TLSv1(HTTPSDummyServerTestCase): - certs = DEFAULT_CERTS.copy() - certs['ssl_version'] = ssl.PROTOCOL_TLSv1 - - def setUp(self): - self._pool = HTTPSConnectionPool(self.host, self.port) - self.addCleanup(self._pool.close) - - 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', '/') + 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() self.assertEqual(conn.cert_reqs, ssl.CERT_REQUIRED) + def test_tls_protocol_name_of_socket(self): + if self.tls_protocol_name is None: + pytest.skip("Skipping base test class") + + 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) + + +@requiresTLSv1() +class TestHTTPS_TLSv1(TestHTTPS): + tls_protocol_name = 'TLSv1' + certs = TLSv1_CERTS + + +@requiresTLSv1_1() +class TestHTTPS_TLSv1_1(TestHTTPS): + tls_protocol_name = 'TLSv1.1' + certs = TLSv1_1_CERTS + + +@requiresTLSv1_2() +class TestHTTPS_TLSv1_2(TestHTTPS): + tls_protocol_name = 'TLSv1.2' + certs = TLSv1_2_CERTS + + +@requiresTLSv1_3() +class TestHTTPS_TLSv1_3(TestHTTPS): + tls_protocol_name = 'TLSv1.3' + class TestHTTPS_NoSAN(HTTPSDummyServerTestCase): certs = NO_SAN_CERTS @@ -662,5 +700,23 @@ def test_strip_square_brackets_before_validating(self): self.assertEqual(r.status, 200) +class TestHTTPS_IPV6SAN(IPV6HTTPSDummyServerTestCase): + certs = IPV6_SAN_CERTS + + def test_can_validate_ipv6_san(self): + """Ensure that urllib3 can validate SANs with IPv6 addresses in them.""" + try: + import ipaddress # noqa: F401 + except ImportError: + pytest.skip("Only runs on systems with an ipaddress module") + + https_pool = HTTPSConnectionPool('[::1]', self.port, + cert_reqs='CERT_REQUIRED', + ca_certs=IPV6_SAN_CA) + self.addCleanup(https_pool.close) + r = https_pool.request('GET', '/') + self.assertEqual(r.status, 200) + + if __name__ == '__main__': unittest.main() diff --git a/test/with_dummyserver/test_socketlevel.py b/test/with_dummyserver/test_socketlevel.py index 0274e6e001..9cdad4bdc5 100644 --- a/test/with_dummyserver/test_socketlevel.py +++ b/test/with_dummyserver/test_socketlevel.py @@ -703,7 +703,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)) try: self.assertRaises(ReadTimeoutError, response.read) finally: