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.