Keywords: Python | SSL error | OpenSSL 3 | cryptography downgrade | RFC 5746
Abstract: This article delves into the common SSL error 'unsafe legacy renegotiation disabled' in Python, which typically occurs when using OpenSSL 3 to connect to servers that do not support RFC 5746. It begins by analyzing the technical background, including security policy changes in OpenSSL 3 and the importance of RFC 5746. Then, it details the solution of downgrading the cryptography package to version 36.0.2, based on the highest-scored answer on Stack Overflow. Additionally, supplementary methods such as custom OpenSSL configuration and custom HTTP adapters are discussed, with comparisons of their pros and cons. Finally, security recommendations and best practices are provided to help developers resolve the issue effectively while ensuring safety.
In Python development, when using the requests library or other HTTP clients to connect to certain HTTPS servers, you may encounter the following error: requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: /api (Caused by SSLError(SSLError(1, '[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled (_ssl.c:997)'))). This error commonly occurs on macOS, Linux, or Windows systems, especially when the client uses OpenSSL 3 and the server does not support the RFC 5746 secure renegotiation standard. This article analyzes the causes of this error from a technical perspective and provides multiple solutions, with a focus on the highest-scored answer from Stack Overflow.
Technical Background Analysis
SSL/TLS renegotiation is a mechanism in the protocol that allows clients and servers to renegotiate encryption parameters on an established connection. However, legacy renegotiation has security vulnerabilities, such as the prefix attack described in CVE-2009-3555, where attackers could exploit it for man-in-the-middle attacks. To enhance security, RFC 5746 introduced the secure renegotiation standard, requiring both clients and servers to support extension mechanisms. OpenSSL 3, as the latest version, disables unsafe legacy renegotiation by default to enforce RFC 5746. When a client using OpenSSL 3 connects to a server that only supports legacy renegotiation, this error is triggered.
In the Python environment, the cryptography package is a core dependency for handling SSL/TLS, as it wraps the OpenSSL library. By default, Python's ssl module uses the system-installed OpenSSL or the version provided by cryptography. If the cryptography package is newer (e.g., version 38.x or higher), it may depend on OpenSSL 3, leading to compatibility issues with older servers. The error message includes _ssl.c:997, pointing to an internal OpenSSL code location, indicating that the issue stems from library-level security policies.
Primary Solution: Downgrading the Cryptography Package
According to the best answer on Stack Overflow (score 10.0), the most straightforward solution is to downgrade the cryptography package to version 36.0.2. This version typically uses OpenSSL 1.x, which allows unsafe legacy renegotiation, thereby ensuring compatibility with older servers. Implementation steps are as follows:
- First, check the currently installed version of
cryptography. Run in the terminal:pip show cryptography. If the version is higher than 36.0.2, a downgrade is needed. - Downgrade the
cryptographypackage. Use the pip command:pip install cryptography==36.0.2. This will install the specified version in the current Python environment. If using a virtual environment (e.g., venv or conda), ensure the command is executed in the activated environment. - Verify the downgrade success. Re-run
pip show cryptographyto confirm the version has changed to 36.0.2. Then, attempt to run the original Python code; the error should be resolved.
Below is an example code snippet demonstrating how to handle HTTPS requests in Python; after downgrading, no code modifications are usually required:
import requests
# Assuming this is the original request code
try:
response = requests.get('https://ssd.jpl.nasa.gov/api/horizons.api', params={'format': 'text', 'EPHEM_TYPE': 'OBSERVER'})
print(response.text)
except requests.exceptions.SSLError as e:
print(f"SSL error occurred: {e}")The advantage of downgrading cryptography is its simplicity and speed, with no need to modify application code. However, it has drawbacks: downgrading may introduce security risks, as older versions might contain unpatched vulnerabilities; additionally, if the project depends on other packages requiring newer cryptography versions, dependency conflicts may arise. Therefore, it is recommended to use this method only in testing or temporary environments and to contact server administrators promptly to upgrade for RFC 5746 support.
Supplementary Solutions
Beyond downgrading cryptography, other methods can resolve this error, offering more flexibility and control. Here are two common approaches:
Custom OpenSSL Configuration
By setting the environment variable OPENSSL_CONF to point to a custom OpenSSL configuration file, unsafe legacy renegotiation can be enabled. Create a file, e.g., custom_openssl.cnf, with the following content:
openssl_conf = openssl_init
[openssl_init]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
Options = UnsafeLegacyRenegotiationThen, set the environment variable before running the Python script: export OPENSSL_CONF=/path/to/custom_openssl.cnf (on Unix systems) or set OPENSSL_CONF=C:\path\to\custom_openssl.cnf (on Windows). This method allows global adjustment of OpenSSL behavior but also carries security risks, and the configuration may be overwritten during OpenSSL updates.
Using a Custom HTTP Adapter
In Python code, you can create a custom HTTP adapter to modify the SSL context, enabling the OP_LEGACY_SERVER_CONNECT option. Since Python's ssl module does not yet directly support this constant, the bit value 0x4 can be used. Example code:
import requests
import urllib3
import ssl
class CustomHttpAdapter(requests.adapters.HTTPAdapter):
def __init__(self, ssl_context=None, **kwargs):
self.ssl_context = ssl_context
super().__init__(**kwargs)
def init_poolmanager(self, connections, maxsize, block=False):
self.poolmanager = urllib3.poolmanager.PoolManager(
num_pools=connections, maxsize=maxsize,
block=block, ssl_context=self.ssl_context)
def get_legacy_session():
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ctx.options |= 0x4 # Enable OP_LEGACY_SERVER_CONNECT
session = requests.session()
session.mount('https://', CustomHttpAdapter(ctx))
return session
# Use the custom session
session = get_legacy_session()
response = session.get('https://ssd.jpl.nasa.gov/api/horizons.api', params={'format': 'text'})
print(response.text)This method provides code-level control, avoiding the impact of global configurations, but requires modifying application code and also involves security compromises. It is suitable for scenarios requiring fine-grained control over SSL behavior.
Security Recommendations and Best Practices
When implementing any solution, security should be the primary consideration. Unsafe legacy renegotiation may expose connections to attacks, so the following measures are recommended:
- Prioritize Server Upgrades: If possible, contact server administrators to upgrade the SSL/TLS implementation to support RFC 5746. This is the safest long-term solution, eliminating compatibility issues and enhancing overall security.
- Use Temporary Solutions: If downgrading
cryptographyor modifying configurations is necessary, use them only in development or testing environments and migrate to secure solutions as soon as possible. In production environments, avoid enabling unsafe options. - Monitor and Update: Regularly check for updates to
cryptographyand OpenSSL to apply security patches. Use tools likepip-auditto identify vulnerabilities in dependencies. - Code Review: If using custom adapters, ensure the code is reviewed to avoid introducing other security vulnerabilities. For example, validate SSL certificates and hostnames to prevent man-in-the-middle attacks.
In summary, resolving the unsafe legacy renegotiation disabled error requires balancing compatibility and security. By understanding the technical background and selecting appropriate solutions, developers can effectively address this issue while maintaining application security standards.