Skip to content

ssl.HAS_TLSv1_2 is False with OpenSSL 4.0 although TLS 1.2 connections work #159387

Open

Description

@lpyu001

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_2

Actual 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
#endif

The 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

Linked PRs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions