Skip to content

fix: enable legacy-renegotiation TLS compat on demand for ISY-994#508

Merged
shbatm merged 2 commits into
v3.x.xfrom
fix/isy994-legacy-renegotiation
May 8, 2026
Merged

fix: enable legacy-renegotiation TLS compat on demand for ISY-994#508
shbatm merged 2 commits into
v3.x.xfrom
fix/isy994-legacy-renegotiation

Conversation

@shbatm
Copy link
Copy Markdown
Collaborator

@shbatm shbatm commented May 8, 2026

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_DISABLED after 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_CONNECT on the existing SSL context only when the peer itself 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.

What changes

  • get_sslcontext() does not pre-set OP_LEGACY_SERVER_CONNECT. Strict TLS by default.
  • Connection.request() catches UNSAFE_LEGACY_RENEGOTIATION_DISABLED specifically: flips the flag on the existing context, logs a one-time WARNING, and retries.
  • All other ClientSSLError failures still raise ISYConnectionError immediately (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

Approach Modern peers ISY-994
Unconditional flag in get_sslcontext Silently weakened MITM-during-renegotiation posture Works
On-demand (this PR) Stay strict forever One failed handshake at startup, logged WARNING, then works

The 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:

WARNING Enabling ISY-994 legacy-renegotiation TLS compatibility for this controller; eisy/Polisy IoX peers do not need this. Original error: ... UNSAFE_LEGACY_RENEGOTIATION_DISABLED ...
INFO ISY Loaded Configuration
INFO Total Loading time: 5.43s

Test plan

  • pytest -q (443 passed)
  • Pre-commit clean
  • Smoke-tested against an ISY-994 at TLS 1.2 on Ubuntu OpenSSL 3.0.13
  • New regression tests:
    • test_get_sslcontext_does_not_preset_legacy_renegotiation — defaults stay strict
    • test_request_legacy_reneg_failure_enables_compat_and_retries — flag flips + retry on the specific error
    • test_request_legacy_reneg_does_not_trigger_for_unrelated_ssl_errors — generic SSL failures don't silently weaken posture

🤖 Generated with Claude Code

shbatm added 2 commits May 8, 2026 09:45
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.
@shbatm shbatm merged commit 2a0da16 into v3.x.x May 8, 2026
4 checks passed
@shbatm shbatm deleted the fix/isy994-legacy-renegotiation branch May 8, 2026 15:37
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.

1 participant