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
Lean on SSLContext to do HTTPS hostname verification #517
Comments
+1 |
Currently, it appears that because urllib3 doesn't use this, an exception is raised. I haven't had a chance to isolate this yet, but a Traceback (most recent call last):
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/basecommand.py", line 232, in main
status = self.run(options, args)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/commands/install.py", line 339, in run
requirement_set.prepare_files(finder)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/req/req_set.py", line 333, in prepare_files
upgrade=self.upgrade,
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/index.py", line 305, in find_requirement
page = self._get_page(main_index_url, req)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/index.py", line 783, in _get_page
return HTMLPage.get_page(link, req, session=self.session)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/index.py", line 872, in get_page
"Cache-Control": "max-age=600",
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/sessions.py", line 473, in get
return self.request('GET', url, **kwargs)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/download.py", line 365, in request
return super(PipSession, self).request(method, url, *args, **kwargs)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/sessions.py", line 461, in request
resp = self.send(prep, **send_kwargs)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/sessions.py", line 573, in send
r = adapter.send(request, **kwargs)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/cachecontrol/adapter.py", line 43, in send
resp = super(CacheControlAdapter, self).send(request, **kw)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/adapters.py", line 370, in send
timeout=timeout
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/packages/urllib3/connectionpool.py", line 518, in urlopen
body=body, headers=headers)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/packages/urllib3/connectionpool.py", line 322, in _make_request
self._validate_conn(conn)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/packages/urllib3/connectionpool.py", line 727, in _validate_conn
conn.connect()
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/packages/urllib3/connection.py", line 238, in connect
ssl_version=resolved_ssl_version)
File "/usr/local/lib/python2.7/site-packages/pip-6.0.7-py2.7.egg/pip/_vendor/requests/packages/urllib3/util/ssl_.py", line 254, in ssl_wrap_socket
return context.wrap_socket(sock)
File "/usr/local/lib/python2.7/ssl.py", line 350, in wrap_socket
_context=self)
File "/usr/local/lib/python2.7/ssl.py", line 537, in __init__
raise ValueError("check_hostname requires server_hostname")
ValueError: check_hostname requires server_hostname |
Followup: This is pip 6.0.7 (obvious from the traceback), which uses requests 2.5.1 (see pypa/pip@ec51b69) which in turn uses a urllib3 checkout with SHA @a27758625e4169330fcf965652b1093faf5aaaa2 (see kennethreitz/requests@d2d576b). Should a new ticket be opened? Or should this be tracked under a different issue? |
It's a new ticket, and it's what we should have opened when I closed kennethreitz/requests#2435. |
Looks like the problem is:
if self.assert_fingerprint:
assert_fingerprint(self.sock.getpeercert(binary_form=True),
self.assert_fingerprint)
elif context.verify_mode != ssl.CERT_NONE \
and not getattr(context, 'check_hostname', False) \
and self.assert_hostname is not False:
# While urllib3 attempts to always turn off hostname matching from
# the TLS library, this cannot always be done. So we check whether
# the TLS Library still thinks it's matching hostnames.
cert = self.sock.getpeercert()
if not cert.get('subjectAltName', ()):
warnings.warn((
'Certificate for {0} has no `subjectAltName`, falling back to check for a '
'`commonName` for now. This feature is being removed by major browsers and '
'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 '
'for details.)'.format(hostname)),
SubjectAltNameWarning
)
_match_hostname(cert, self.assert_hostname or server_hostname)
self.is_verified = (
context.verify_mode == ssl.CERT_REQUIRED or
self.assert_fingerprint is not None
) no DO REPEAT YOURSELF logic is going to be called. class CustomAdapter(requests.adapters.HTTPAdapter):
def __init__(self, poolmanager_server_hostname, *args, **kwargs):
self.poolmanager_server_hostname = poolmanager_server_hostname
requests.adapters.HTTPAdapter.__init__(self, *args, **kwargs)
def init_poolmanager(self, *args, **kwargs):
self.poolmanager = requests.adapters.PoolManager(
*args,
server_hostname=self.poolmanager_server_hostname,
**kwargs)
s = requests.Session()
prefix = 'https://<ip_addr>'
server_certificate_hostname = '<some_obfuscated_hostname_inside_self_signed_certificate>'
s.mount(prefix, CustomAdapter(poolmanager_server_hostname=server_certificate_hostname))
with s.request(<parameters>)
<etc> |
Maybe I'm not understanding your comment but I wanted to make clear that we do hostname verification of certs, not just fingerprinting. When an I can't speak for requests but if you want to accomplish what I believe you want to do with just urllib3 you can do this: import urllib3
pool = urllib3.PoolManager(
cert_reqs='REQUIRED',
assert_hostname=server_certificate_hostname,
# If you want SNI to emit this hostname as well you need this too:
# server_hostname=server_certificate_hostname
)
r = pool.request('GET', 'https://<ip address>') If you have more questions about how we do hostname verification, SNI, or TLS I can answer them. |
@sethmlarson, yeah seems like assert_hostname is what needed!
import requests
class CustomAdapter(requests.adapters.HTTPAdapter):
def __init__(self, *args, assert_hostname=None, **kwargs):
self.assert_hostname = assert_hostname
requests.adapters.HTTPAdapter.__init__(self, *args, **kwargs)
def init_poolmanager(self, *args, **kwargs):
self.poolmanager = requests.adapters.PoolManager(
*args,
assert_hostname=self.assert_hostname,
**kwargs)
s = requests.Session()
prefix = 'https://<ip_addr>'
s.mount(prefix, CustomAdapter(assert_hostname=False))
with s.request(<parameters>) as resp:
<etc>
|
2+3. This is up to you, you've got your adapter written already and now you can just use and extend it if you want. urllib3 is intended to be a low-level interface and requests is a higher-level interface. |
Any progress on leveraging stdlib's logic? |
I don't think so, this issue is up for grabs still :) |
Couple of things that came up during my initial work-through of this change:
|
(The way I'm seeing this is that removing |
An additional complication is that |
So there are three things that urllib3 does that are not possible by setting
If we decide to keep the first two, as I think we should, then this is issue is not a breaking change and can always be handled later in v2, alongside #2111. (And it will be easier when we drop Python 3.6 support.) |
Talked with Seth and he's worried that doing this later breaks weird use cases that call our internal but documented functions. So I'll work on this as part of 2.0, not 2.x. Another complication: pyOpenSSL only learned recently how to check hostnames in a simple way. But the release that includes that change also drops support for OpenSSL 1.0.2. So we're also going to check hostnames manually with pyOpenSSL. And when we'll drop support for OpenSSL 1.0.2, we'll also require pyOpenSSL >= 20.0.0. |
Actually, this is fine: we need to maintain our own implementation anyway to remove commonName support. |
In Python 3.4 and 2.7.9 there is a
check_hostname
attribute onSSLContext
which will have the SSLContext instance handle checking the hostname inside of thedo_handshake
. I think it would be good for urllib3 to rely on this where possible instead of doing the check itself. I think this would go good with the other efforts to useSSLContext
as the "bag of configuration" for TLS stuff.This can be detected by determining if the
SSLContext
object has acheck_hostname
attribute or not.There is one downside, this relies on passing the hostname as part of
SSLContext().wrap_socket(server_name=)
. Originally this only worked if the OpenSSL had SNI enabled. However Python 3.4.3 and 2.7.9 will acceptserver_name
even if SNI isn't enabled.This would mean that the code a little ugly until you drop older versions of Python 3, it would look something like (paraphrased and super simplified to just be the ugly parts):
Maybe this is a bad idea? I think it would be great to lay the ground work for eventually moving the responsibility for checking hostnames into the stdlib, however you wouldn't actually be able to do that completely without the ugly if statement until 3.4.3 was your minimum supports Python 3 version.
The text was updated successfully, but these errors were encountered: