Repository navigation
ssl.HAS_TLSv1_2 is False with OpenSSL 4.0 although TLS 1.2 connections work #159387
Description
Bug report
Summary
When _ssl is built against OpenSSL 4.0, ssl.HAS_TLSv1_2 is False, yet a context limited to maximum_version = TLSVersion.TLSv1_2 negotiates a TLS 1.2 connection. The same compile-time condition makes _ssl treat TLS 1.3 as the minimum available protocol. As a result, TLSVersion.MINIMUM_SUPPORTED resolves to TLS 1.3, and test_ssl skips its TLS 1.2 tests.
Reproduction
import os, socket, ssl, threading
import test
print(ssl.OPENSSL_VERSION)
print("HAS_TLSv1_2:", ssl.HAS_TLSv1_2)
certfile = os.path.join(os.path.dirname(test.__file__), "certdata", "keycert.pem")
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_ctx.load_cert_chain(certfile)
server_ctx.maximum_version = ssl.TLSVersion.TLSv1_2
client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_ctx.check_hostname = False
client_ctx.verify_mode = ssl.CERT_NONE
client_ctx.maximum_version = ssl.TLSVersion.TLSv1_2
listener = socket.create_server(("127.0.0.1", 0))
def serve():
conn, _ = listener.accept()
with server_ctx.wrap_socket(conn, server_side=True) as s:
s.recv(1)
t = threading.Thread(target=serve)
t.start()
with client_ctx.wrap_socket(socket.create_connection(listener.getsockname())) as s:
print("negotiated:", s.version(), s.cipher())
s.send(b"x")
t.join()
listener.close()
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.maximum_version = ssl.TLSVersion.MINIMUM_SUPPORTED
print("maximum_version = MINIMUM_SUPPORTED ->", repr(ctx.maximum_version))./python -m test test_ssl -v -m test_psk -m test_min_max_version_tlsv1_2 \
-m test_min_max_version_mismatch -m test_protocol_tlsv1_2Actual Behavior
OpenSSL 4.0.2 25 Aug 2026
HAS_TLSv1_2: False
negotiated: TLSv1.2 ('ECDHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256)
maximum_version = MINIMUM_SUPPORTED -> <TLSVersion.TLSv1_3: 772>
test_min_max_version_mismatch (test.test_ssl.ThreadedTests.test_min_max_version_mismatch) ... skipped 'TLSv1_2 is not available.'
test_min_max_version_tlsv1_2 (test.test_ssl.ThreadedTests.test_min_max_version_tlsv1_2) ... skipped 'TLSv1_2 is not available.'
Connecting to a TLSv1.2 server with various client options. ... skipped 'TLSv1_2 is not available.'
test_psk (test.test_ssl.ThreadedTests.test_psk) ... skipped 'TLSv1_2 is not available.'
Expected Behavior
The ssl documentation defines HAS_TLSv1_2 as "Whether the OpenSSL library has built-in support for the TLS 1.2 protocol." OpenSSL 4.0.2 still supports TLS 1.2: the handshake above succeeds, and openssl ciphers -s -tls1_2 lists TLS 1.2 cipher suites such as ECDHE-ECDSA-AES256-GCM-SHA384. So HAS_TLSv1_2 should be True. TLSVersion.MINIMUM_SUPPORTED should map to a version no higher than TLS 1.2, and the TLS 1.2 tests in test_ssl should run instead of being skipped.
Root cause
Modules/_ssl.c contains this block, added for OpenSSL 4.0 in gh-146207:
#if (OPENSSL_VERSION_NUMBER >= 0x40000000L)
# define OPENSSL_NO_SSL3
# define OPENSSL_NO_TLS1
# define OPENSSL_NO_TLS1_1
# define OPENSSL_NO_TLS1_2
# define OPENSSL_NO_SSL3_METHOD
# define OPENSSL_NO_TLS1_METHOD
# define OPENSSL_NO_TLS1_1_METHOD
# define OPENSSL_NO_TLS1_2_METHOD
#endifThe OpenSSL 4.0 compatibility block mixes up protocol availability with fixed-version method availability.
OPENSSL_NO_TLS1_2_METHOD means TLSv1_2_method() is unavailable, while OPENSSL_NO_TLS1_2 means the TLS 1.2 protocol itself is unavailable. OpenSSL 4.0 removed the deprecated fixed-version methods, but TLS 1.0, 1.1 and 1.2 remain usable through TLS_method().
Because _ssl.c defines the protocol-level macros as well, CPython treats TLS 1.0–1.2 as unavailable. This makes ssl.HAS_TLSv1_2 false, causes TLSVersion.MINIMUM_SUPPORTED to resolve to TLS 1.3, and makes TLS 1.2 tests guarded by requires_tls_version() get skipped, even though a TLS 1.2 handshake succeeds.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Activity