diff --git a/CHANGES.md b/CHANGES.md index 5b2c9731..05b00fbd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * Change API response header for JSON content type and the return of JSONP data * Remove redundant MultipartPostHandler * Update Beautiful Soup 4.4.0 (r390) to 4.4.0 (r397) +* Update backports/ssl_match_hostname 3.4.0.2 to 3.5.0.1 (r18) ### 0.11.0 (2016-01-10 22:30:00 UTC) diff --git a/lib/backports/ssl_match_hostname/README.txt b/lib/backports/ssl_match_hostname/README.txt index de3910a6..1b61cac6 100644 --- a/lib/backports/ssl_match_hostname/README.txt +++ b/lib/backports/ssl_match_hostname/README.txt @@ -1,8 +1,8 @@ -The ssl.match_hostname() function from Python 3.4 +The ssl.match_hostname() function from Python 3.5 ================================================= -The Secure Sockets layer is only actually *secure* +The Secure Sockets Layer is only actually *secure* if you check the hostname in the certificate returned by the server to which you are connecting, and verify that it matches to hostname @@ -21,31 +21,48 @@ Simply make this distribution a dependency of your package, and then use it like this:: from backports.ssl_match_hostname import match_hostname, CertificateError - ... - sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv3, + [...] + sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv23, cert_reqs=ssl.CERT_REQUIRED, ca_certs=...) try: match_hostname(sslsock.getpeercert(), hostname) except CertificateError, ce: ... -Note that the ``ssl`` module is only included in the Standard Library -for Python 2.6 and later; -users of Python 2.5 or earlier versions -will also need to install the ``ssl`` distribution -from the Python Package Index to use code like that shown above. - Brandon Craig Rhodes is merely the packager of this distribution; -the actual code inside comes verbatim from Python 3.4. +the actual code inside comes from Python 3.5 with small changes for +portability. + + +Requirements +------------ + +* If you want to verify hosts match with certificates via ServerAltname + IPAddress fields, you need to install the `ipaddress module`_. + backports.ssl_match_hostname will continue to work without ipaddress but + will only be able to handle ServerAltName DNSName fields, not IPAddress. + System packagers (Linux distributions, et al) are encouraged to add + this as a hard dependency in their packages. + +* If you need to use this on Python versions earlier than 2.6 you will need to + install the `ssl module`_. From Python 2.6 upwards ``ssl`` is included in + the Python Standard Library so you do not need to install it separately. + +.. _`ipaddress module`:: https://pypi.python.org/pypi/ipaddress +.. _`ssl module`:: https://pypi.python.org/pypi/ssl History ------- + * This function was introduced in python-3.2 * It was updated for python-3.4a1 for a CVE (backports-ssl_match_hostname-3.4.0.1) * It was updated from RFC2818 to RFC 6125 compliance in order to fix another security flaw for python-3.3.3 and python-3.4a5 (backports-ssl_match_hostname-3.4.0.2) +* It was updated in python-3.5 to handle IPAddresses in ServerAltName fields + (something that backports.ssl_match_hostname will do if you also install the + ipaddress library from pypi). .. _RFC2818: http://tools.ietf.org/html/rfc2818.html diff --git a/lib/backports/ssl_match_hostname/__init__.py b/lib/backports/ssl_match_hostname/__init__.py index 34f248f3..06538ec6 100644 --- a/lib/backports/ssl_match_hostname/__init__.py +++ b/lib/backports/ssl_match_hostname/__init__.py @@ -1,8 +1,20 @@ """The match_hostname() function from Python 3.3.3, essential when using SSL.""" import re +import sys + +# ipaddress has been backported to 2.6+ in pypi. If it is installed on the +# system, use it to handle IPAddress ServerAltnames (this was added in +# python-3.5) otherwise only do DNS matching. This allows +# backports.ssl_match_hostname to continue to be used all the way back to +# python-2.4. +try: + import ipaddress +except ImportError: + ipaddress = None + +__version__ = '3.5.0.1' -__version__ = '3.4.0.2' class CertificateError(ValueError): pass @@ -61,6 +73,23 @@ def _dnsname_match(dn, hostname, max_wildcards=1): return pat.match(hostname) +def _to_unicode(obj): + if isinstance(obj, str) and sys.version_info < (3,): + obj = unicode(obj, encoding='ascii', errors='strict') + return obj + +def _ipaddress_match(ipname, host_ip): + """Exact matching of IP addresses. + + RFC 6125 explicitly doesn't define an algorithm for this + (section 1.7.2 - "Out of Scope"). + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + # Divergence from upstream: ipaddress can't handle byte str + ip = ipaddress.ip_address(_to_unicode(ipname).rstrip()) + return ip == host_ip + + def match_hostname(cert, hostname): """Verify that *cert* (in decoded format as returned by SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 @@ -70,12 +99,35 @@ def match_hostname(cert, hostname): returns nothing. """ if not cert: - raise ValueError("empty or no certificate") + raise ValueError("empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED") + try: + # Divergence from upstream: ipaddress can't handle byte str + host_ip = ipaddress.ip_address(_to_unicode(hostname)) + except ValueError: + # Not an IP address (common case) + host_ip = None + except UnicodeError: + # Divergence from upstream: Have to deal with ipaddress not taking + # byte strings. addresses should be all ascii, so we consider it not + # an ipaddress in this case + host_ip = None + except AttributeError: + # Divergence from upstream: Make ipaddress library optional + if ipaddress is None: + host_ip = None + else: + raise dnsnames = [] san = cert.get('subjectAltName', ()) for key, value in san: if key == 'DNS': - if _dnsname_match(value, hostname): + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == 'IP Address': + if host_ip is not None and _ipaddress_match(value, host_ip): return dnsnames.append(value) if not dnsnames: