Specific security level and additional CA certificates when using Python requests

I maintain a Python library to work with the API of one pretty popular Russian 🇷🇺 service.
Recently, this service started to have some limitations related to TLS and CA certificates:

  • First of all, you need to have specific CA certificates to work with it and people use these certificates only in Russia so, it means if you want to work with this service then you need to preinstall them because you will not find them in the basic CA suit, that requests library uses. That's the first problem I had.
  • Another one is this service, for some reason, started working only with some specific cipher suites. That's another limitation that I needed to satisfy.

So, in the end, I found a way how to solve these problems and I wanted to share it because it could be useful.

Use additional CA certificates with Python requests

Python requests uses Python certifi to get access to CA certificates, it's just a text file with a bunch of certificates.

I considered different ways of how to start using additional CA in a separate library and eventually, decided to keep my file with certificates in the library and use it instead of the original file from Python certify. It's pretty simple and reliable.

# You can use `verify` parameter in requests method to pass a file path to your file with CA certificates.
response = requests.get('your_url', verify='your_path_to_certificates')

# or you can create a specific Session class(I took this code from my library)
import os
from importlib.util import find_spec

from requests import Session

import rosreestr_api


class CustomSession(Session):
    CACERT_PATH = os.path.join(
        os.path.dirname(find_spec(rosreestr_api.__name__).origin),
        'cacert.pem'
    )
    def __init__(self):
        super().__init__()
        self.verify = self.CACERT_PATH

Use a specific cipher suites with Python requests

In order to do that, we can use HTTPS adapter and override init_poolmanager method.

import ssl

from requests import Session
from requests.adapters import HTTPAdapter


class HTTPSAdapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        ssl_context = ssl.create_default_context()
        # https://www.openssl.org/docs/man3.0/man3/SSL_CTX_set_security_level.html
        # rosreestr supports only SECLEVEL 1
        ssl_context.set_ciphers('DEFAULT@SECLEVEL=1')
        # We want to use the most secured protocol from security level 1
        ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
        kwargs['ssl_context'] = ssl_context
        return super().init_poolmanager(*args, **kwargs)


class CustomSession(Session):
    def __init__(self):
        super().__init__()
        self.mount(prefix='https://', adapter=HTTPSAdapter())

Final version

import ssl
import os
from importlib.util import find_spec

from requests import Session
from requests.adapters import HTTPAdapter

import rosreestr_api


class HTTPSAdapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        ssl_context = ssl.create_default_context()
        # https://www.openssl.org/docs/man3.0/man3/SSL_CTX_set_security_level.html
        # rosreestr supports only SECLEVEL 1
        ssl_context.set_ciphers('DEFAULT@SECLEVEL=1')
        # We want to use the most secured protocol from security level 1
        ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
        kwargs['ssl_context'] = ssl_context
        return super().init_poolmanager(*args, **kwargs)


class CustomSession(Session):
    CACERT_PATH = os.path.join(
        os.path.dirname(find_spec(rosreestr_api.__name__).origin),
        'cacert.pem'
    )
    def __init__(self):
        super().__init__()
        self.verify = self.CACERT_PATH
        self.mount(prefix='https://', adapter=HTTPSAdapter())

So, in this way, everything works fine.