[BUG] extract_peer_certificate() raises AttributeError with pyOpenSSL >= 26.2.0 when the peer certificate has extensions
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- cryptography, python
- Área
- api, authentication, backend
Línea de trabajo
Start with extract_peer_certificate() and create_transport_details() in the Twisted and asyncio paths, then read twisted/websocket.py and twisted/rawsocket.py at their handshake callbacks. The issue identifies pyOpenSSL's removed get_extension() call and describes the expected certificate fields and transport behavior. Done means certificate extraction works with current pyOpenSSL, peer_cert is populated after TLS handshakes, and the listed real-certificate tests pass.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Analysis across autobahn-python (master b7c3646d) and Crossbar.io (master 4d2a46fb + fix_2048), with pyOpenSSL 26.4.0, Twisted 26.4.0, cryptography 50.0.2.
Two bugs, not one
A. extract_peer_certificate() raises on every real certificate (this issue). It calls X509.get_extension(i), removed in pyOpenSSL 26.2.0 (get_extension_count() is still there, so the loop is entered). It also calls X509.get_subject() / get_issuer(), deprecated in pyOpenSSL 26.x ("in preparation for deprecating pyOpenSSL's X509 entirely"). Only a certificate without any extensions gets through, which in practice is none (SAN, key usage, basic constraints).
B. On WebSocket and RawSocket (Twisted), peer_cert is never filled. create_transport_details() runs in connectionMade(), and Twisted calls the wrapped protocol's connectionMade() on TCP connect, before the TLS handshake. After the handshake, _onOpen() (twisted/websocket.py) and _on_handshake_complete() (twisted/rawsocket.py) refresh channel_id, but not peer_cert. Bug B hides bug A on these transports. Checked with a TLS server that requires a client certificate, on both TLS 1.2 and TLS 1.3:
connectionMade peer_cert: None channel_id: 00000000...
handshakeCompleted EXC AttributeError 'X509' object has no attribute 'get_extension'
dataReceived EXC AttributeError 'X509' object has no attribute 'get_extension'
Both bugs date from bbdadcb5 (#1552, 2022-04-27, "add TLS extract_peer_certificate helper; fill in transport details"). Before that, Crossbar.io read the client certificate itself.
Who is affected
| Caller | Effect |
|---|---|
autobahn twisted/websocket.py, twisted/rawsocket.py → create_transport_details() in connectionMade() |
peer_cert always None (bug B) |
autobahn asyncio/util.py create_transport_details() |
peer_cert hard-coded None (asyncio never extracts it) |
Crossbar.io router/auth/tls.py (PendingAuthTLS, authmethod tls), reads transport_details.peer_cert["sha1"] |
over WebSocket / RawSocket: always "no client certificate presented" (bug B), since 22.x |
Crossbar.io dynamic authenticators: details["transport"]["peer_cert"] (via TransportDetails.marshal(), router/auth/pending.py) |
same, always null |
Crossbar.io MQTT bridge WampTransport (bridge/mqtt/wamp.py, crossbar #2048): builds its transport details after the handshake |
AttributeError, the connection fails (bug A); the two MQTT TLS tests are skipped for now |
So TLS client-certificate authentication in Crossbar.io does not work on any transport today, and no test catches it: neither project has a test with a real TLS client certificate. Crossbar.io's test_authorize.py::test_authextra_tls uses mocks.
Other pyOpenSSL X509 uses in Crossbar.io are unaffected: crypto.load_certificate() in common/twisted/endpoint.py and common/twisted/tlsctx.py still exists.
Recommended fix (autobahn 26.10.2)
-
Re-implement the extraction on
cryptography.x509(a core autobahn dependency,cryptography>=50), as a backend-neutral helper that takes acryptography.x509.Certificate:- Twisted:
transport.getPeerCertificate().to_cryptography()(to_cryptography()exists since pyOpenSSL 17.1, the floor is 20.0.1). - asyncio:
ssl_object.getpeercert(binary_form=True)→x509.load_der_x509_certificate(). This fillspeer_certfor asyncio too, so both backends behave the same.
- Twisted:
-
Keep the returned dict as it is. It is an interface: dynamic authenticators get it, and Crossbar.io's
tlsauthmethod keys on"sha1". Field by field, checked against the current pyOpenSSL output:field replacement same as before? md5,sha1,sha256":".join(f"{b:02X}" for b in cert.fingerprint(hashes.SHA1()))yes, byte-identical serial,versioncert.serial_number,cert.version.valueyes not_before,not_aftercert.not_valid_before_utc.strftime("%Y%m%d%H%M%SZ")yes expirednow > cert.not_valid_after_utcyes signature_algorithmcert.signature_algorithm_oid._nameyes for RSA and ECDSA; ed25519instead ofED25519subject,issuerper attribute: rfc4514_attribute_name(C,ST,L,O,OU,CN), else (emailAddresshas no RFC 4514 name)oid._name; lowercasedyes, including emailaddresshashX509.subject_name_hash()(OpenSSLX509_NAME_hash; not deprecated, no cryptography equivalent)yes on Twisted; Noneon asyncioextensions[].name,.criticalext.oid._name,ext.criticalyes for the standard extensions extensions[].valuewas OpenSSL's text ( DNS:localhost, IP Address:127.0.0.1); cryptography has no such printerchanges: SAN can be rendered OpenSSL-style, other extensions as str(ext.value) -
Never raise on the peer's certificate. This runs on data the peer controls, before authentication. cryptography rejects for example duplicate extensions when
cert.extensionsis read; catch that, log it, and return the other fields withextensions: [], so the connection still gets to authentication. -
Fix bug B: in
_onOpen()(WebSocket) and_on_handshake_complete()(RawSocket) setpeer_certnext tochannel_id. TLS application data only arrives after the handshake, so the certificate is there by then. -
Tests with real TLS client certificates, generated at test time: the extraction on a certificate with SAN, key usage, EKU and an email address, comparing the dict field by field; WebSocket and RawSocket over TLS deliver
peer_certto the session; a malformed certificate does not raise; the asyncio path.
Then Crossbar.io 26.10.1
- Raise the floor to
autobahn>=26.10.2, relock. - Un-skip the two MQTT TLS tests (#2048).
- Add a router test: authmethod
tlsover RawSocket and over WebSocket with a real client certificate, both accepted and denied, so this cannot regress unnoticed again. - Changelog: TLS client-certificate authentication works again on all transports.
Not in scope: tls-unique is not defined for TLS 1.3 (RFC 9266 tls-exporter), see crossbar #2037.
This analysis was done with AI assistance (Claude Code).
- Lenguaje dominante
- Python
- Estrellas
- 2.5k
- Forks
- 767
- Merge medio
- 1 d 1 h
- PR fusionados (30 d)
- 23
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de crossbario/autobahn-python
-
CI-CD enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
crossbario/autobahn-python#1945 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug CI-CD
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
crossbario/autobahn-python#1932 ·
Los mantenedores suelen responder en 1 día
-
CI-CD enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
crossbario/autobahn-python#1829 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug CI-CD
Dificultad 4/5 3-5 días Aptitud para principiantes 52/100
crossbario/autobahn-python#2013 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
crossbario/autobahn-python#1963 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
Todos los issues de crossbario/autobahn-python
Issues similares
-
changelog investigate
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
ramnes/notion-sdk-py#423 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 77/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 1-3 horas Aptitud para principiantes 89/100
duckdb/duckdb-web#7514 ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
jbacalso24/mimry#525 ·
Los mantenedores suelen responder en 1 día