fix: enable legacy-renegotiation TLS compat on demand for ISY-994#508
Merged
Conversation
ISY-994 firmware's TLS stack pre-dates RFC 5746 and does not advertise the secure-renegotiation extension. OpenSSL 3.x (default on Ubuntu 22.04+ / RHEL 9 / current Python distros) refuses such peers with ``UNSAFE_LEGACY_RENEGOTIATION_DISABLED`` even after a successful version+cipher negotiation, so HTTPS to an ISY-994 cannot complete a handshake on a stock modern host — which is the entire promise of the v3.6.0 auto-TLS modernization. We can't identify peer class before the handshake (no pre-auth metadata; eisy/Polisy IoX vs ISY-994 looks the same on the wire until the handshake completes). Rather than degrade everyone unconditionally, ``Connection.request()`` now flips ``OP_LEGACY_SERVER_CONNECT`` on the existing SSL context only when the peer rejects the handshake with the specific ``UNSAFE_LEGACY_RENEGOTIATION_DISABLED`` failure, and retries once. Modern peers (eisy/Polisy IoX with TLS 1.3, or ISY-994 firmware that honors RFC 5746) never reach that branch and stay strict for the lifetime of the ``Connection``. The first failed handshake on an ISY-994 logs a one-time WARNING explaining the degradation. Smoke-tested against an ISY-994 at TLS 1.2: previously failed on modern OpenSSL with ``UNSAFE_LEGACY_RENEGOTIATION_DISABLED``; now logs the WARNING once and completes initialize() in ~5s.
``ssl.OP_LEGACY_SERVER_CONNECT`` was added to the stdlib in Python 3.12, so referencing it directly raised ``AttributeError`` on the 3.11 CI job. The underlying OpenSSL flag has the stable value ``0x4``; expose ``OP_LEGACY_SERVER_CONNECT`` as a module-level constant in ``pyisy.connection`` via ``getattr(ssl, ..., 0x4)`` and use that through both production code and tests. No behavior change on 3.12+ — the getattr resolves to the real attribute. On 3.11 the literal carries the same flag value so the on-demand TLS-renegotiation compat works identically.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ISY-994 firmware's TLS stack pre-dates RFC 5746 and does not advertise the secure-renegotiation extension. OpenSSL 3.x (default on Ubuntu 22.04+ / RHEL 9 / current Python distros) refuses such peers with
UNSAFE_LEGACY_RENEGOTIATION_DISABLEDafter a successful version+cipher negotiation — so on a stock modern host, HTTPS to an ISY-994 cannot complete a handshake even with v3.6.0's auto-TLS landing, defeating the modernization.We can't identify peer class before the handshake (eisy/Polisy IoX vs ISY-994 looks identical on the wire until the handshake completes). Rather than degrade everyone unconditionally, this PR flips
OP_LEGACY_SERVER_CONNECTon the existing SSL context only when the peer itself rejects the handshake with the specificUNSAFE_LEGACY_RENEGOTIATION_DISABLEDfailure, and retries once. Modern peers (eisy/Polisy IoX with TLS 1.3, or ISY-994 firmware that honors RFC 5746) never reach that branch and stay strict for the lifetime of theConnection.What changes
get_sslcontext()does not pre-setOP_LEGACY_SERVER_CONNECT. Strict TLS by default.Connection.request()catchesUNSAFE_LEGACY_RENEGOTIATION_DISABLEDspecifically: flips the flag on the existing context, logs a one-time WARNING, and retries.ClientSSLErrorfailures still raiseISYConnectionErrorimmediately (per fix: raise ISYConnectionError on SSL/TLS handshake failures #507) — protocol mismatches and cert verification failures are real config errors, not legacy compat needs.Why on-demand instead of unconditional
get_sslcontextThe on-demand approach uses peer behavior as the discriminator — the ISY-994 self-identifies through the failure signature, no protocol guessing needed.
Smoke test (ISY-994 at TLS 1.2, modern Ubuntu / OpenSSL 3.0.13)
Before #494/#499/#507 stack: opaque debug log, generic ISYConnectionError, HTTPS unusable.
After #507 alone: clean
ISYConnectionError: SSL/TLS error: ... UNSAFE_LEGACY_RENEGOTIATION_DISABLED ...— informative, but HTTPS still unusable.After this PR:
Test plan
pytest -q(443 passed)test_get_sslcontext_does_not_preset_legacy_renegotiation— defaults stay stricttest_request_legacy_reneg_failure_enables_compat_and_retries— flag flips + retry on the specific errortest_request_legacy_reneg_does_not_trigger_for_unrelated_ssl_errors— generic SSL failures don't silently weaken posture🤖 Generated with Claude Code