Search code examples
pythonsslcertificateclient-certificatesaiohttp

aiohttp and client-side SSL certificates


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 !


Solution

  • 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.