Description
Documentation
SSLSocket.shared_ciphers()
does not document None
is returned on session reuse
Summary
The fix of #96931 resulted in a change in SSLSocket.shared_ciphers()
.
If the session is reused, SSLSocket.shared_ciphers()
returns None
Proposal: update documentation of SSLSocket.shared_ciphers()
to note None
is returned on session reuse.
Background & Motivation
As an example, here is a sample server.py
and client.py
scripts to show the behavior change in Python 3.11.2 and 3.11.3:
# server.py
import socket
import ssl
import platform
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile="ca.pem")
context.load_cert_chain(certfile="server.pem")
port = 12345
bindsocket = socket.socket()
bindsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
bindsocket.bind(("localhost", port))
bindsocket.listen(5)
print("Python version: {}".format(platform.python_version()))
print("server listening on port {}".format(port))
while True:
newsocket, fromaddr = bindsocket.accept()
connstream: ssl.SSLContext.sslsocket_class = context.wrap_socket(
newsocket, server_side=True, do_handshake_on_connect=True)
print("server got connection on address: {}".format(fromaddr))
print("server shared ciphers: {}".format(connstream.shared_ciphers()))
print("server session reused? {}".format(connstream.session_reused))
data = connstream.recv(1024)
while data:
print("server got data {}".format(data))
data = connstream.recv(1024)
print("server finished with client")
connstream.close()
# client.py
import socket
import ssl
port = 12345
"""
Use TLS 1.2 so session ticket is sent.
https://docs.python.org/3/library/ssl.html#ssl-session describes:
> Session tickets are no longer sent as part of the initial handshake and are handled differently. SSLSocket.session and SSLSession are not compatible with TLS 1.3.
"""
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2)
context.load_verify_locations(cafile="ca.pem")
conn: ssl.SSLSocket = context.wrap_socket(socket.socket(socket.AF_INET),
server_hostname="localhost")
conn.connect(("localhost", port))
conn.write(b"foo")
assert not conn.session_reused
session = conn.session
conn.close()
# Connect again and reuse the session.
conn = context.wrap_socket(socket.socket(socket.AF_INET),
server_hostname="localhost",
session=session)
conn.connect(("localhost", port))
conn.write(b"foo")
assert conn.session_reused
conn.close()
Here is the output of server.py
on Python 3.11.2:
Python version: 3.11.2
server listening on port 12345
server got connection on address: ('127.0.0.1', 64310)
server shared ciphers: [('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256), ('TLS_CHACHA20_POLY1305_SHA256', 'TLSv1.3', 256), ('TLS_AES_128_GCM_SHA256', 'TLSv1.3', 128), ('ECDHE-ECDSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('ECDHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('ECDHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('ECDHE-ECDSA-CHACHA20-POLY1305', 'TLSv1.2', 256), ('ECDHE-RSA-CHACHA20-POLY1305', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES256-SHA384', 'TLSv1.2', 256), ('ECDHE-RSA-AES256-SHA384', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES128-SHA256', 'TLSv1.2', 128), ('ECDHE-RSA-AES128-SHA256', 'TLSv1.2', 128), ('DHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('DHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('DHE-RSA-AES256-SHA256', 'TLSv1.2', 256), ('DHE-RSA-AES128-SHA256', 'TLSv1.2', 128)]
server session reused? False
server got data b'foo'
server finished with client
server got connection on address: ('127.0.0.1', 64311)
server shared ciphers: [('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256), ('TLS_CHACHA20_POLY1305_SHA256', 'TLSv1.3', 256), ('TLS_AES_128_GCM_SHA256', 'TLSv1.3', 128), ('ECDHE-ECDSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('ECDHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('ECDHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('ECDHE-ECDSA-CHACHA20-POLY1305', 'TLSv1.2', 256), ('ECDHE-RSA-CHACHA20-POLY1305', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES256-SHA384', 'TLSv1.2', 256), ('ECDHE-RSA-AES256-SHA384', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES128-SHA256', 'TLSv1.2', 128), ('ECDHE-RSA-AES128-SHA256', 'TLSv1.2', 128), ('DHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('DHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('DHE-RSA-AES256-SHA256', 'TLSv1.2', 256), ('DHE-RSA-AES128-SHA256', 'TLSv1.2', 128)]
server session reused? True
server got data b'foo'
server finished with client
Here is the output of server.py
on Python 3.11.3:
Python version: 3.11.3
server listening on port 12345
server got connection on address: ('127.0.0.1', 64316)
server shared ciphers: [('ECDHE-ECDSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('ECDHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('ECDHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('ECDHE-ECDSA-CHACHA20-POLY1305', 'TLSv1.2', 256), ('ECDHE-RSA-CHACHA20-POLY1305', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES256-SHA384', 'TLSv1.2', 256), ('ECDHE-RSA-AES256-SHA384', 'TLSv1.2', 256), ('ECDHE-ECDSA-AES128-SHA256', 'TLSv1.2', 128), ('ECDHE-RSA-AES128-SHA256', 'TLSv1.2', 128), ('DHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256), ('DHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128), ('DHE-RSA-AES256-SHA256', 'TLSv1.2', 256), ('DHE-RSA-AES128-SHA256', 'TLSv1.2', 128)]
server session reused? False
server got data b'foo'
server finished with client
server got connection on address: ('127.0.0.1', 64317)
server shared ciphers: None
server session reused? True
server got data b'foo'
server finished with client
In 3.11.3, after the session is reused, the return of shared_ciphers()
is None
. The scripts and test certificates are located here.
Alternatives
openssl/openssl#4295 suggest alternative API to use in OpenSSL:
Ah, the shared ciphers would only be computed when negotiation is performed (i.e., not resumption), yes. Hopefully you can update to a supported version of OpenSSL and pick up the needed funcitonality.
An alternative implementation could be to use SSL_CTX_set_client_hello_cb
to obtain the list of ciphers sent in the ClientHello. Store the list of ciphers for retrieval in shared_ciphers()
. #110902 implements this change but is (at present) left as draft. Storing the ciphers requires additional memory per socket and may not provide much value to users. Instead, #106345 proposes a documentation only change to inform users.
Known Impact
The None
return resulted in a bug report in PyKMIP. The call to shared_ciphers
was not checking for the None
return value: OpenKMIP/PyKMIP#700