Skip to content

fix: handle TLS 1.3 post-handshake auth errors in _loopback_for_cert_thread (closes #209)#824

Open
botbikamordehai2-sketch wants to merge 1 commit into
cherrypy:mainfrom
botbikamordehai2-sketch:fix/issue-209-1781094197
Open

fix: handle TLS 1.3 post-handshake auth errors in _loopback_for_cert_thread (closes #209)#824
botbikamordehai2-sketch wants to merge 1 commit into
cherrypy:mainfrom
botbikamordehai2-sketch:fix/issue-209-1781094197

Conversation

@botbikamordehai2-sketch

Copy link
Copy Markdown

What

When a client uses the TLS 1.3 post_handshake_auth extension and authentication fails, the server can receive an SSL error after the handshake is complete. This exception is currently unhandled in the _loopback_for_cert_thread function, causing a traceback that propagates to the communicate method and breaks request parsing.

Fix

Wrap the wrap_socket call inside _loopback_for_cert_thread in an additional try/except block that catches ssl.SSLError exceptions specifically related to post-handshake authentication failures (e.g., decrypt errors, TLSv1 alert). The existing suppress context manager continues to handle connection/OS errors. The new handler only suppresses known post-handshake TLS 1.3 errors and re-raises unexpected ones.

Closes #209

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
242 2 240 14
View the full list of 2 ❄️ flaky test(s)
cheroot/test/test_ssl.py::test_https_over_http_error[0.0.0.0]

Flake rate in main: 6.90% (Passed 162 times, Failed 12 times)

Stack Traces | 0.21s run time
http_server = <generator object http_server.<locals>.start_srv at 0x105170ba0>
ip_addr = '0.0.0.0'

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[90m#x1B[39;49;00m
        #x1B[33m'#x1B[39;49;00m#x1B[33mip_addr#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        (#x1B[90m#x1B[39;49;00m
            ANY_INTERFACE_IPV4,#x1B[90m#x1B[39;49;00m
            ANY_INTERFACE_IPV6,#x1B[90m#x1B[39;49;00m
        ),#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_https_over_http_error#x1B[39;49;00m(http_server, ip_addr):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Ensure that connecting over HTTPS to HTTP port is handled."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        httpserver = http_server.send((ip_addr, EPHEMERAL_PORT))#x1B[90m#x1B[39;49;00m
        interface, _host, port = _get_conn_data(httpserver.bind_addr)#x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m pytest.raises(ssl.SSLError) #x1B[94mas#x1B[39;49;00m ssl_err:#x1B[90m#x1B[39;49;00m
            http.client.HTTPSConnection(#x1B[90m#x1B[39;49;00m
                #x1B[33mf#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m{#x1B[39;49;00minterface#x1B[33m}#x1B[39;49;00m#x1B[33m:#x1B[39;49;00m#x1B[33m{#x1B[39;49;00mport#x1B[33m}#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
            ).request(#x1B[33m'#x1B[39;49;00m#x1B[33mGET#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        expected_substring = (#x1B[90m#x1B[39;49;00m
            #x1B[33m'#x1B[39;49;00m#x1B[33mrecord layer failure#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m IS_ABOVE_OPENSSL31#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m #x1B[33m'#x1B[39;49;00m#x1B[33mwrong version number#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m IS_ABOVE_OPENSSL10#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m #x1B[33m'#x1B[39;49;00m#x1B[33munknown protocol#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
>       #x1B[94massert#x1B[39;49;00m expected_substring #x1B[95min#x1B[39;49;00m ssl_err.value.args[-#x1B[94m1#x1B[39;49;00m]#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE       AssertionError: assert 'record layer failure' in '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1131)'#x1B[0m

_host      = '0.0.0.0'
expected_substring = 'record layer failure'
http_server = <generator object http_server.<locals>.start_srv at 0x105170ba0>
httpserver = <cheroot.server.HTTPServer object at 0x105176f40>
interface  = '127.0.0.1'
ip_addr    = '0.0.0.0'
port       = 49628
ssl_err    = <ExceptionInfo SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1131)') tblen=10>

#x1B[1m#x1B[31mcheroot/test/test_ssl.py#x1B[0m:707: AssertionError
cheroot/test/test_ssl.py::test_https_over_http_error[::]

Flake rate in main: 10.53% (Passed 17 times, Failed 2 times)

Stack Traces | 0.147s run time
http_server = <generator object http_server.<locals>.start_srv at 0x104e3f890>
ip_addr = '::'

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[90m#x1B[39;49;00m
        #x1B[33m'#x1B[39;49;00m#x1B[33mip_addr#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        (#x1B[90m#x1B[39;49;00m
            ANY_INTERFACE_IPV4,#x1B[90m#x1B[39;49;00m
            ANY_INTERFACE_IPV6,#x1B[90m#x1B[39;49;00m
        ),#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_https_over_http_error#x1B[39;49;00m(http_server, ip_addr):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Ensure that connecting over HTTPS to HTTP port is handled."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        httpserver = http_server.send((ip_addr, EPHEMERAL_PORT))#x1B[90m#x1B[39;49;00m
        interface, _host, port = _get_conn_data(httpserver.bind_addr)#x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m pytest.raises(ssl.SSLError) #x1B[94mas#x1B[39;49;00m ssl_err:#x1B[90m#x1B[39;49;00m
            http.client.HTTPSConnection(#x1B[90m#x1B[39;49;00m
                #x1B[33mf#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m{#x1B[39;49;00minterface#x1B[33m}#x1B[39;49;00m#x1B[33m:#x1B[39;49;00m#x1B[33m{#x1B[39;49;00mport#x1B[33m}#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
            ).request(#x1B[33m'#x1B[39;49;00m#x1B[33mGET#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        expected_substring = (#x1B[90m#x1B[39;49;00m
            #x1B[33m'#x1B[39;49;00m#x1B[33mrecord layer failure#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m IS_ABOVE_OPENSSL31#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m #x1B[33m'#x1B[39;49;00m#x1B[33mwrong version number#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m IS_ABOVE_OPENSSL10#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m #x1B[33m'#x1B[39;49;00m#x1B[33munknown protocol#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
>       #x1B[94massert#x1B[39;49;00m expected_substring #x1B[95min#x1B[39;49;00m ssl_err.value.args[-#x1B[94m1#x1B[39;49;00m]#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE       AssertionError: assert 'record layer failure' in '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1131)'#x1B[0m

_host      = '::'
expected_substring = 'record layer failure'
http_server = <generator object http_server.<locals>.start_srv at 0x104e3f890>
httpserver = <cheroot.server.HTTPServer object at 0x105186a00>
interface  = '::1'
ip_addr    = '::'
port       = 49639
ssl_err    = <ExceptionInfo SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1131)') tblen=10>

#x1B[1m#x1B[31mcheroot/test/test_ssl.py#x1B[0m:707: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 cheroot | 🛠️ Build #33074856 | 📁 Comparing eea7790 against latest (2ffb0ba)

  🔍 Preview build  

3 files changed
± history/index.html
± pkg/cheroot.server/index.html
± pkg/cheroot.ssl.pyopenssl/index.html

@avinashkamat48-design avinashkamat48-design left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new except ssl.SSLError branch does not actually preserve unexpected SSL errors because it is nested inside the existing outer with suppress(ssl.SSLError, OSError): block. Even when _assert_ssl_exc_contains(...) returns false and the code does
aise, that re-raised SSLError is immediately swallowed by the surrounding suppress, so behavior remains 'suppress every SSLError'. To make the filter meaningful, the selective ry/except needs to live outside the broad suppress or the outer suppress needs to stop including ssl.SSLError.

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.

Improve support for TLS 1.3 post-handshake auth

2 participants