Httpx: How can I use a custom SSLContext / PyOpenSSLContext when creating a Client?

Created on 3 May 2020  路  5Comments  路  Source: encode/httpx

Checklist

Question

Can I create a httpx.Client with my own ssl.SSLContext or urllib3.contrib.pyopenssl.PyOpenSSLContext instead of passing in cert/key/verify?

Background

I found httpx while looking for requests syntax + asyncio support. It looks like a great project, thanks for all the work you've put into it.

One very oft-asked feature for requests.py was making requests with user-provided SSLContexts 2118. Eventually that was resolved by allowing us to pass SSLContexts to Adapters, then mounting the adapter onto a session. As a real world example, I have used pypki2 and requests_pkcs12 at different times to create Session's from PKCS12/X509 certificates instead of PEM format that I believe vanilla requests and httpx require. There is a requests_pkcs12 author blog post with more background.

My understanding from reading httpx documentation is that a httpx.Client is roughly similar to a requests.Session and the Dispatcher API will be roughly similar to Adapters. @tomchristie mentions configuring an ssl_context in 768 - Dispatcher API but I didn't understand how to use that in practice.

@sethmlarson suggests httpx.Client(verify=ssl_context) in 469, which looked similar to my use-case but not identical. When I tried that with httpx 0.12.1 and 0.13.0.dev, I got TypeError: expected str, bytes or os.PathLike object, not PyOpenSSLContext on Client init.

Thanks.

(courtesy tagging @rashley-iqt (requests_pkcs12) and @gershwinlabs (pypki2) )

Most helpful comment

Thanks for bringing this use-case to my attention. Got me thinking how this use case can be reconciled with my other thoughts on safe high level TLS APIs.

All 5 comments

Thanks for bringing this use-case to my attention. Got me thinking how this use case can be reconciled with my other thoughts on safe high level TLS APIs.

Heya,

How can I use a custom SSLContext / PyOpenSSLContext when creating a Client?

Yup, the verify argument optionally takes an SSLContext instance. So let's thrash out if there's an issue here or not.

One thing that I did notice prompted by this ticket, is that even though that's a supported usage, and is type annotated as bool|str|SSLContext, the SSLContext usage isn't actually currently noted in the docs. https://www.python-httpx.org/advanced/#ssl-certificates

That's something we should clearly improve. We ought to include a very minimal possible example, such as...

>>> import httpx
>>> import ssl
>>> import certifi
>>> context = ssl.create_default_context()
>>> context.load_verify_locations(cafile=certifi.where())
>>> httpx.get('https://www.example.com', verify=context)
<Response [200 OK]>

I got TypeError: expected str, bytes or os.PathLike object, not PyOpenSSLContext on Client init.

Could you include a traceback with that?

We don't have any instances of PathLike in the httpx codebase, and the example above works as expected, so it's not immediately obvious how to replicate this?

Thanks for the reply @tomchristie. I went back and did some more tests on httpx 0.12.1 and 0.13.0.dev. It looks like the verify= syntax does work for both httpx.get(url, verify=context) and httpx.Client(verify=context).get(url) for ssl.SSLContext objects generated by pypki2. I should have tested that initially.

For requests_pkcs12 generated urllib3.contrib.pyopenssl.PyOpenSSLContext objects, I get the same general error. Here's the full traceback from httpx 0.12.1:

import requests_pkcs12
content = open('/home/jovyan/.devpki/mrkafon.p12', 'rb').read()
context = requests_pkcs12.create_ssl_context(content, b"changeme")
context.load_verify_locations(cafile='/home/jovyan/.devpki/vast-ca.pem')
context
>>> <urllib3.contrib.pyopenssl.PyOpenSSLContext at 0x7fe95baafa90>

url = "https://internal-site.com"
resp = httpx.get(url, verify=context)
resp
>>>

TypeError                                 Traceback (most recent call last)
<ipython-input-3-2fa41aa34910> in <module>
      1 url = "https://internal-site.com"
----> 2 resp = httpx.get(url, verify=context)
      3 resp

/opt/conda/lib/python3.7/site-packages/httpx/_api.py in get(url, params, headers, cookies, auth, allow_redirects, cert, verify, timeout, trust_env)
    166         verify=verify,
    167         timeout=timeout,
--> 168         trust_env=trust_env,
    169     )
    170 

/opt/conda/lib/python3.7/site-packages/httpx/_api.py in request(method, url, params, data, files, json, headers, cookies, auth, timeout, allow_redirects, verify, cert, trust_env)
     80     """
     81     with Client(
---> 82         cert=cert, verify=verify, timeout=timeout, trust_env=trust_env,
     83     ) as client:
     84         return client.request(

/opt/conda/lib/python3.7/site-packages/httpx/_client.py in __init__(self, auth, params, headers, cookies, verify, cert, proxies, timeout, pool_limits, max_redirects, base_url, dispatch, app, trust_env)
    470             dispatch=dispatch,
    471             app=app,
--> 472             trust_env=trust_env,
    473         )
    474         self.proxies: typing.Dict[str, SyncDispatcher] = {

/opt/conda/lib/python3.7/site-packages/httpx/_client.py in init_dispatch(self, verify, cert, pool_limits, dispatch, app, trust_env)
    499 
    500         return URLLib3Dispatcher(
--> 501             verify=verify, cert=cert, pool_limits=pool_limits, trust_env=trust_env,
    502         )
    503 

/opt/conda/lib/python3.7/site-packages/httpx/_dispatch/urllib3.py in __init__(self, proxy, verify, cert, trust_env, pool_limits)
     33     ):
     34         ssl_config = SSLConfig(
---> 35             verify=verify, cert=cert, trust_env=trust_env, http2=False
     36         )
     37         hard_limit = pool_limits.hard_limit

/opt/conda/lib/python3.7/site-packages/httpx/_config.py in __init__(self, cert, verify, trust_env, http2)
     69         self.trust_env = trust_env
     70         self.http2 = http2
---> 71         self.ssl_context = self.load_ssl_context()
     72 
     73     def __eq__(self, other: typing.Any) -> bool:

/opt/conda/lib/python3.7/site-packages/httpx/_config.py in load_ssl_context(self)
     92 
     93         if self.verify:
---> 94             return self.load_ssl_context_verify()
     95         return self.load_ssl_context_no_verify()
     96 

/opt/conda/lib/python3.7/site-packages/httpx/_config.py in load_ssl_context_verify(self)
    121         elif isinstance(self.verify, bool):
    122             ca_bundle_path = self.DEFAULT_CA_BUNDLE_PATH
--> 123         elif Path(self.verify).exists():
    124             ca_bundle_path = Path(self.verify)
    125         else:

/opt/conda/lib/python3.7/pathlib.py in __new__(cls, *args, **kwargs)
   1020         if cls is Path:
   1021             cls = WindowsPath if os.name == 'nt' else PosixPath
-> 1022         self = cls._from_parts(args, init=False)
   1023         if not self._flavour.is_supported:
   1024             raise NotImplementedError("cannot instantiate %r on your system"

/opt/conda/lib/python3.7/pathlib.py in _from_parts(cls, args, init)
    667         # right flavour.
    668         self = object.__new__(cls)
--> 669         drv, root, parts = self._parse_args(args)
    670         self._drv = drv
    671         self._root = root

/opt/conda/lib/python3.7/pathlib.py in _parse_args(cls, args)
    651                 parts += a._parts
    652             else:
--> 653                 a = os.fspath(a)
    654                 if isinstance(a, str):
    655                     # Force-cast str subclasses to str (issue #21127)

TypeError: expected str, bytes or os.PathLike object, not PyOpenSSLContext

If it helps, I'm using my own internal CA to sign the .p12 certs, and that CA is used by the internal server I'm connecting to. I can get to the internal site using the same .p12 and CA with requests_pkcs12. It's still possible I'm messing up the cafile loading though?

Thanks.

If you want custom SSL context support, we do have that, but you need to be using an SSLContext instance, from the stdlib.

We don't have support for PyOpenSSLContext, and the SSL configuration is (correctly) falling through the if isinstance(self.verify, ssl.SSLContext) check, and raising an error at the point that it attempts to treat the context as a string.

https://github.com/encode/httpx/blob/d34c89a81995fccee01a41de057e1f6a1e2a3dfa/httpx/_config.py#L107-L115

I'd suggest you start by looking into setting up a custom SSLContext instance, and passing that to verify=....

If there's some capabilities exposed by the third party pyopenssl package, that aren't also present in Python 3's ssl implementation, then it's feasible that there's a valid feature request in here that we could work towards, tho it looks like pyopenssl is more aimed at improving SSL capabilities for Python 2.7-3.5.

Seconding this, pyopenssl allows the use of loading certificates from memory instead of from disk

Was this page helpful?
0 / 5 - 0 ratings