Repository navigation
Fix 2014 - #2015
Merged
Merged
Fix 2014#2015
Conversation
Start the 26.10.2 development cycle; crossbario#2014 (extract_peer_certificate() with pyOpenSSL >= 26.2.0) ships in autobahn 26.10.2, which Crossbar.io 26.10.1 will depend on. Expected CI result: GREEN (version change only). Note: This work was completed with AI assistance (Claude Code).
…s and channel binding New tests over real TLS connections (loopback, certificates created per test: a CA, a server certificate for localhost, client certificates client_0 and client_1 as in crossbar-examples authentication/tls), on Twisted and asyncio, over WebSocket and RawSocket, on TLS 1.2 and TLS 1.3. The router side is a minimal FakeRouterSession (autobahn/test/tlsutil.py) that authenticates like Crossbar.io: authmethod "tls" by the SHA1 fingerprint of the client certificate (PendingAuthTLS), and WAMP-cryptosign with channel binding "tls-unique" (PendingAuthCryptosign, but denying instead of falling back when the router has no channel ID). Twisted (autobahn/twisted/test/test_tx_tls.py), 21 tests: - extract_peer_certificate() on a client certificate with SAN, key usage and EKU, field by field against values computed from the DER: FAILS (AttributeError: 'X509' object has no attribute 'get_extension', removed in pyOpenSSL 26.2.0) - extract_peer_certificate() on a certificate with a duplicate extension must not raise: FAILS (same) - authmethod "tls" accepted (default, TLS 1.2, TLS 1.3) and an unknown client certificate denied for that reason, WebSocket and RawSocket: FAIL, the router sees no client certificate (peer_cert is only read in connectionMade(), before the TLS handshake) - WAMP-cryptosign with channel binding over RawSocket (4 tests): FAIL, the signature does not match the router's channel ID. The Twisted RawSocket protocols never set is_server, so the client side takes the server's TLS Finished message for its tls-unique channel ID - passing: no client certificate denied (WebSocket, RawSocket), cryptosign with channel binding over WebSocket (4 tests), no peer certificate asyncio (autobahn/asyncio/test/test_aio_tls.py), 26 tests: - authmethod "tls" accepted / unknown certificate denied (8 tests): FAIL, asyncio never fills peer_cert - WAMP-cryptosign with channel binding without a client certificate (6 tests): FAIL, a TLS server connection without a client certificate is not is_secure and has no channel ID (is_secure is derived from the peer certificate) - passing: channel binding with a client certificate (6), no client certificate denied (2) Expected CI result: RED. Twisted trial fails in exactly these 14 tests (locally cpy311: 349 ok, 12 failures, 2 errors; same on pypy311), asyncio pytest in exactly these 14 tests (320 passed, 14 failed); ruff and ty 0.0.89 clean. Note: This work was completed with AI assistance (Claude Code).
patch_modules() rebound sys.modules to a copy and "restored" it to another copy (since 073d33d, 2015), so after the first test of test_tx_choosereactor the sys.modules dict was no longer the one the import system uses. Any module first imported lazily later in the same trial process then fails. On GitHub CI (all CPython jobs) this hit the new crossbario#2014 tests: pyOpenSSL's X509.has_expired() calls datetime.strptime(), whose lazy import of _strptime raised "KeyError: '_strptime'", which hid the actual failure. Locally _strptime happened to be imported earlier, so it did not show. Reproduced locally by running test_tx_choosereactor before test_tx_tls.ExtractPeerCertificateTests in one trial process (2x KeyError: '_strptime'); with mock.patch.dict(sys.modules), which patches and restores the dict in place, the same run fails with the expected AttributeError ('X509' object has no attribute 'get_extension'). Correction to the previous (red) commit message cf8edbc: the asyncio test module has 22 tests (14 fail, 8 pass), not 26. Expected CI result: still RED, in the same 14 Twisted tests, now with the expected errors on CPython too (locally cpy311: 349 ok, 12 failures, 2 errors, no _strptime errors); test_tx_choosereactor passes. Note: This work was completed with AI assistance (Claude Code).
… tests fail In the "Unit Tests" matrix jobs the asyncio step was skipped whenever the Twisted step failed, so the red run of crossbario#2014 showed only the 14 Twisted failures and never the 14 asyncio ones. The asyncio step now runs unless the job was cancelled (if: !cancelled()), so both backends always report. Expected CI result: RED. Unit Tests jobs: Twisted fails in the 14 crossbario#2014 tests (12 failures, 2 errors), and asyncio now runs and fails in the 14 crossbario#2014 tests (locally cpy311: 320 passed, 14 failed). ruff and ty 0.0.89 clean. Note: This work was completed with AI assistance (Claude Code).
…e asyncio tls-unique (RFC 5929) is the first TLS Finished message of the handshake, in a full handshake the client's; Python's ssl module, which autobahn uses on asyncio, returns exactly that. Checked on one connection (TLS 1.2 and 1.3): Twisted's transport_channel_id() gives sha256(server Finished) when is_server is set correctly (WebSocket), and with is_server=None (RawSocket) sha256(client Finished) on the server but sha256(server Finished) on the client. So WAMP-cryptosign channel binding fails between a Twisted router (Crossbar.io) and asyncio clients over WebSocket, and between Twisted RawSocket peers; it works Twisted<->Twisted over WebSocket (both sides use the server's Finished) and Crossbar.io RawSocket<->asyncio (both use the client's). New tests in TlsUniqueTests (autobahn/twisted/test/test_tx_tls.py), 5 FAIL: - a Twisted TLS server and a Python ssl client, TLS 1.2 and TLS 1.3: the server's channel ID must equal the client's - a Twisted TLS client and a Python ssl server, TLS 1.2 and TLS 1.3: same - the WAMP WebSocket and RawSocket server / client protocols set is_server (RawSocket: None) Expected CI result: RED. Twisted trial now fails in 19 crossbario#2014 tests (locally cpy311: 349 ok, 17 failures, 2 errors), asyncio unchanged in the 14 crossbario#2014 tests (320 passed, 14 failed); ruff and ty 0.0.89 clean. Note: This work was completed with AI assistance (Claude Code).
…ed and asyncio
Peer certificate:
- new autobahn.util.peer_certificate_details(): the peer_cert dict, read
with cryptography (pyOpenSSL removed X509.get_extension in 26.2.0 and
deprecates the rest of its X509 API). Same keys and formats as before;
extensions[].value: a subject / issuer alternative name as OpenSSL prints
it (DNS:localhost, IP Address:..), other extensions as cryptography prints
them; signature_algorithm "ed25519" instead of "ED25519". Never raises on
certificate content: unreadable extensions / subject / issuer are empty
- Twisted extract_peer_certificate() uses it (to_cryptography(); hash from
pyOpenSSL's subject_name_hash(), which is not deprecated)
- Twisted WebSocket (_onOpen) and RawSocket (_on_handshake_complete) read
peer_cert after the TLS handshake, next to the channel ID; it was read in
connectionMade(), before the handshake, and so was always None
- asyncio: is_secure from the SSL object (was from "peercert", so a server
without client certificate was not secure and had no channel ID), and new
autobahn.asyncio.util.extract_peer_certificate() fills peer_cert (was
always None)
tls-unique (RFC 5929, as the user decided):
- Twisted transport_channel_id() uses the first Finished message of the
handshake (the client's): server: get_peer_finished(), client:
get_finished(), as CPython's ssl module does. It used the server's
- Twisted RawSocket server / client protocols set is_server (was None, so
the client side took the wrong message); asyncio WebSocket protocols too
- PyPy workaround: PyPy's ssl get_channel_binding("tls-unique") misses the
negation CPython has (reused ^ socket_type instead of ^ !socket_type) and
returns the server's Finished message; its tls_unique_cb() is correct.
New autobahn.util.tls_unique() uses that on PyPy; asyncio uses
tls_unique(), and TlsUniqueTests compare Twisted against it, so they pass
on PyPy too (they failed there against the plain ssl module, as on the
pypy311 job of the previous red push). PyPy issue drafted.
Changelog: 26.10.2 section, with the extension value format, the tls-unique
change (Twisted peers on <= 26.10.1 and >= 26.10.2 no longer agree over
WebSocket when they use channel binding) and the PyPy workaround.
This is substantially new code (peer_certificate_details, tls_unique and
the asyncio extraction) written by AI on request; it needs human review.
Expected CI result: GREEN. Locally: Twisted trial cpy311 368 ok / pypy311
367 ok, asyncio pytest cpy311 334 passed / pypy311 333 passed, all crossbario#2014
tests pass on both; import smoke test 35 passed; ruff and ty 0.0.89 clean;
docs build ok, no new warnings.
Note: This work was completed with AI assistance (Claude Code).
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.
fixes #2014