Skip to content

Support Python linked against OpenSSL 4 in tls_versions handling - #108

Open
faratech wants to merge 1 commit into
mysql:trunkfrom
faratech:openssl4-tls-versions
Open

faratech wants to merge 1 commit into
mysql:trunkfrom
faratech:openssl4-tls-versions

Conversation

@faratech

@faratech faratech commented Oct 10, 2026 •

Copy link
Copy Markdown

Problem

When Python is linked against OpenSSL 4 (Ubuntu 26.10's Python 3.14.8, for example), the ssl module no longer defines the deprecated PROTOCOL_TLSv1, PROTOCOL_TLSv1_1 or PROTOCOL_TLSv1_2. Three modules read these constants at import time:

  • mysql/connector/network.py
  • mysql/connector/aio/network.py
  • mysqlx/connection.py

The try blocks around them only catch ImportError, so both packages fail to import:

>>> import mysql.connector
  File ".../mysql/connector/network.py", line 46, in <module>
    "TLSv1": ssl.PROTOCOL_TLSv1,
AttributeError: module 'ssl' has no attribute 'PROTOCOL_TLSv1'. Did you mean: 'PROTOCOL_TLS'?

>>> import mysqlx
  File ".../mysqlx/connection.py", line 42, in <module>
AttributeError: module 'ssl' has no attribute 'PROTOCOL_TLSv1_2'. Did you mean: 'PROTOCOL_TLS'?

This happens with the latest release (26.7.0) and with current trunk, and it affects every application, including ones that never pass tls_versions.

Fix

When tls_versions is given, build the context from ssl.PROTOCOL_TLS_CLIENT and express the requested versions with SSLContext.minimum_version / maximum_version. This replaces the version-specific protocols and the deprecated OP_NO_TLSv1* options. Both APIs exist on every supported Python (3.10+, which requires OpenSSL 1.1.1 or newer), so the change also works on OpenSSL 1.1.1 and 3.x.

The change is backwards compatible:

  • Negotiation is unchanged. ["TLSv1.2"] is still capped at TLS 1.2. A list that includes TLSv1.3 stays uncapped, as it was with PROTOCOL_TLS.
  • System policy is kept. The version range is only ever narrowed: the minimum is raised and the maximum lowered, never the reverse. A stricter system-wide setting, such as OpenSSL MinProtocol = TLSv1.3 (which Debian/Ubuntu and Fedora/RHEL Pythons honor), still applies, as it did with the version-specific protocols. Setting minimum_version unconditionally would silently allow TLS 1.2 on such systems.
  • Certificate verification is unchanged. The classic connectors already set check_hostname and verify_mode explicitly after creating the context. The X DevAPI path explicitly keeps CERT_NONE unless VERIFY_IDENTITY or ssl-ca is used, which matches what the old PROTOCOL_TLSv1_2/PROTOCOL_TLS contexts did by default.
  • The TLSv1/TLSv1.1 map entries are removed. They were unreachable, because option validation already rejects both versions as UNACCEPTABLE_TLS_VERSIONS.
  • Fewer warnings. The deprecated PROTOCOL_TLSv1_2, PROTOCOL_TLS and OP_NO_* usages, and the DeprecationWarnings they emit on 3.10+, are gone from these paths.

Tests

The PR adds:

  • test_tls_versions_negotiated in test_connection.py, test_aio_connection.py and test_mysqlx_connection.py. Each test connects with every tls_versions combination and asserts the exact negotiated version from Ssl_version / Mysqlx_ssl_version. The existing test_get_connection_with_tls_version / test_get_session_with_tls_version tests are skipped on servers newer than 8.0.27, so tls_versions currently has no coverage against current servers.
  • test_build_ssl_context_tls_versions and test_build_ssl_context_keeps_stricter_default in test_network.py. These check the context's version range, and that a stricter default minimum is not lowered.

Upstream suite

Modules network, connection, aio_connection and mysqlx_connection were run with unittests.py against a bootstrapped MySQL Server 26.7.0, using the pure-Python implementation:

Code Python / OpenSSL Result
trunk 3.10.22 / OpenSSL 3.5.9 baseline
trunk + new tests 3.10.22 / OpenSSL 3.5.9 the new negotiation tests pass, so they encode the existing behavior
this PR 3.10.22 / OpenSSL 3.5.9 identical to baseline per test, plus the 5 new tests passing
trunk 3.14.8 / OpenSSL 4.0.3 AttributeError on import (all modules)
this PR 3.14.8 / OpenSSL 4.0.3 classic and asyncio: identical per test to OpenSSL 3.5.9. mysqlx: all TLS tests identical, but 20 other tests hit the separate Python 3.14 zstd issue described below

To confirm the mysqlx result, I reran it with a local workaround for that zstd issue (a fresh decompressor per frame). The results then match OpenSSL 3.5.9 except test_compression_sizes, which fails on the compressing side of the same zstd issue (5174: Invalid compressed frame).

A mutation check confirmed the new tests catch both kinds of regression:

  • TLSv1.2 cap removed: all four negotiation tests fail. Against the server, ["TLSv1.2"] negotiates TLSv1.3.
  • Unconditional minimum_version: test_build_ssl_context_keeps_stricter_default fails.

TLS behavior comparison

To compare trunk and this PR directly, I ran their TLS setup code (build_ssl_context + switch_to_ssl for sync and asyncio, SocketStream.set_ssl for mysqlx) through real handshakes against local TLS servers: 1.2-only, 1.3-only, 1.2–1.3, and 1.2–1.3 with an untrusted certificate. The cases cover every combination of tls_versions (none, 1.2, 1.3, both), ssl_verify_cert, ssl_verify_identity / X DevAPI ssl-mode, ssl_ca, hostname match or mismatch, and tls_ciphersuites. That is 1,360 cases per run, and each records the negotiated version and cipher, or the failure reason:

Comparison Differences
trunk vs this PR, Python 3.10.22 + OpenSSL 3.5.9 0 (exact outcome text, including error reasons)
trunk vs this PR, Python 3.14.8 + OpenSSL 3.5.9 0 (exact outcome text)
this PR, OpenSSL 3.5.9 vs OpenSSL 4.0.3, same Python 3.14.8 0 (outcome, version and cipher)
this PR under MinProtocol = TLSv1.3, vs trunk's results restricted to TLS 1.3 0 (never negotiates TLS 1.2)

Unrelated pre-existing issues found while testing

These appear on unmodified trunk too, and are not changed here:

  • The certificates in tests/data/ssl have expired, so test_connect_with_unix_socket (sync and asyncio) and mysqlx's test_ssl_connection error with CERTIFICATE_EXPIRED on every configuration.
  • On Python 3.14, mysqlx/protocol.py uses the stdlib compression.zstd. Its ZstdDecompressor handles a single frame, and the compressor reuses one instance across messages, so X Protocol zstd_stream compression raises EOFError: Already at the end of a Zstandard frame. on the second compressed message.
  • On Python 3.14, the asyncio MySQLSocket.switch_to_ssl() calls StreamWriter.start_tls() without server_hostname. With ssl_verify_identity=True it fails with ValueError: check_hostname requires server_hostname, whether or not tls_versions is given. It works on 3.10, which uses the connector's own workaround. I tested only 3.10 and 3.14, but 3.11+ presumably fails the same way, since that is where the native start_tls() is used.

OCA

I have signed the Oracle Contributor Agreement (Mike Fara), and it is pending approval.

🤖 Generated with Claude Code

Python built against OpenSSL 4 no longer defines the deprecated
ssl.PROTOCOL_TLSv1, PROTOCOL_TLSv1_1 and PROTOCOL_TLSv1_2 constants. The
classic, asyncio and X DevAPI network modules read them at import time,
so `import mysql.connector` and `import mysqlx` fail with AttributeError
before any connection is attempted.

When tls_versions is given, build the context from
ssl.PROTOCOL_TLS_CLIENT and express the requested versions as a
minimum_version/maximum_version range, replacing the version-specific
protocols and the deprecated OP_NO_TLSv1* options. Both APIs are present
on every supported Python (3.10+) with OpenSSL 1.1.1, 3.x and 4.

The range is only ever narrowed: the minimum is raised and the maximum
lowered, never the reverse. A stricter system-wide policy, such as
OpenSSL's MinProtocol (which distribution Pythons honor), therefore still
applies, just as it did with the version-specific protocols.

Negotiation is unchanged: a request limited to TLSv1.2 is still capped at
TLSv1.2, and a request that includes TLSv1.3 stays uncapped as it was with
PROTOCOL_TLS. Certificate verification is unchanged too. The classic
connectors already set check_hostname and verify_mode explicitly, and the
X DevAPI keeps CERT_NONE unless VERIFY_IDENTITY or ssl-ca is used.

The TLSv1 and TLSv1.1 map entries are removed. They were unreachable,
because option validation rejects both versions as unacceptable.

Add tests that assert the exact negotiated TLS version for each
tls_versions combination, and that a stricter default minimum is kept.
The existing tls_versions tests are skipped on servers newer than 8.0.27,
so this path had no coverage on current servers.
@faratech
faratech force-pushed the openssl4-tls-versions branch from 40a253b to d7a79c8 Compare October 10, 2026 06:32
@mysql-oca-bot

Copy link
Copy Markdown

Hi, thank you for submitting this pull request. In order to consider your code we need you to sign the Oracle Contribution Agreement (OCA). Please review the details and follow the instructions at https://oca.opensource.oracle.com/
Please make sure to include your MySQL bug system user (email) in the returned form.
Thanks

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants