12

I recently moved off from flask + requests onto aiohttp and its async http client.

In my scenario, I need to make a call to an API over HTTPS (with custom certificates) AND send a client-side certificate along.

For the first part (validating custom certs), the support is clear clearly documented int the docs and it works great.

On the other hand, for the second part, I can't seem to be able to find an easy way of attaching a custom SSL client-side certificate to authorise the client.

Do you guys know how to do that ? Many thanks !

Udi
  • 29,222
  • 9
  • 96
  • 129
Seeker89
  • 282
  • 1
  • 2
  • 9

1 Answers1

35

EDIT: I've submitted a PR with an update to the aiohttp documentation regarding the subject, and it's been merged.

For anyone who might encounter this issue in the future..

TL:DR

import ssl
import aiohttp    

ssl_ctx = ssl.create_default_context(cafile='/path_to_client_root_ca')
ssl_ctx.load_cert_chain('/path_to_client_public_key.pem', '/path_to_client_private_key.pem')

conn = aiohttp.TCPConnector(ssl_context=ssl_ctx)
session = aiohttp.ClientSession(connector=conn)

# session will now send client certificates..

The long story - I've looked how it's implemented in requests (which neatly documents the API here), and apparently it's implemented inside of urllib3.

urllib3 trickles down the cert parameter all the way down to its HTTPSConnection object, where it eventually calls this function:

...
self.sock = ssl_wrap_socket(
    sock=conn,
    keyfile=self.key_file,
    certfile=self.cert_file,
    ssl_context=self.ssl_context,
)
...

which does:

...
if ca_certs or ca_cert_dir:
    try:
        context.load_verify_locations(ca_certs, ca_cert_dir)
    except IOError as e:  # Platform-specific: Python 2.6, 2.7, 3.2
        raise SSLError(e)
    # Py33 raises FileNotFoundError which subclasses OSError
    # These are not equivalent unless we check the errno attribute
    except OSError as e:  # Platform-specific: Python 3.3 and beyond
        if e.errno == errno.ENOENT:
            raise SSLError(e)
        raise
elif getattr(context, 'load_default_certs', None) is not None:
    # try to load OS default certs; works well on Windows (require Python3.4+)
    context.load_default_certs()

if certfile:
    context.load_cert_chain(certfile, keyfile)
if HAS_SNI:  # Platform-specific: OpenSSL with enabled SNI
    return context.wrap_socket(sock, server_hostname=server_hostname)
...

The interesting call here is to load_cert_chain - this means that if we just create an ssl.SSLContext (which is a standard library interface) object and call load_cert_chain with our client certificates like so, aiohttp will behave the same as requests\urllib3.

So although aiohttp's documentation is lacking in telling you that, they do specify that you can load your own ssl.SSLContext.

OmerBA
  • 792
  • 8
  • 13
  • 3
    Note, I couldn't get this working when the server tries to authenticate the client unless I added a purpose: `ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)`. Leaving this here for future reference! – Daniel Oct 05 '18 at 15:47
  • We had a problem with verification of remote server certs similar to [this issue](https://github.com/aio-libs/aiohttp/issues/3180). So we ended up loading the server's root cert like so: `ssl_ctx = ssl.create_default_context(cafile='certs/entrust_ev_ca.cer')` In this scenario, is it okay to then add on the client cert/key? `ssl_ctx.load_cert_chain('client.crt', 'client.key')` I ask because in the example I assume that the call to `create_default_context` refers to the client's root CA, not the server's... – Jason Capriotti Sep 05 '19 at 21:45
  • This has worked for me until Python 3.9 with 3.10 and 3.11 client certificates are stopped working with `Cannot create a client socket with a PROTOCOL_TLS_SERVER context (_ssl.c:801)`. Any idea how to get it working again? – Daniel Sass May 02 '23 at 09:22