Skip to content

SSLSocket.shared_ciphers() does not document None is returned on session reuse #106344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
kevinAlbs opened this issue Jul 3, 2023 · 0 comments · May be fixed by #106345 or #110902
Open

SSLSocket.shared_ciphers() does not document None is returned on session reuse #106344

kevinAlbs opened this issue Jul 3, 2023 · 0 comments · May be fixed by #106345 or #110902
Labels
3.11 only security fixes 3.12 only security fixes 3.13 bugs and security fixes docs Documentation in the Doc dir topic-SSL

Comments

@kevinAlbs
Copy link

kevinAlbs commented Jul 3, 2023

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

Linked PRs

@kevinAlbs kevinAlbs added the docs Documentation in the Doc dir label Jul 3, 2023
@kevinAlbs kevinAlbs linked a pull request Jul 3, 2023 that will close this issue
@serhiy-storchaka serhiy-storchaka added topic-SSL 3.11 only security fixes 3.12 only security fixes 3.13 bugs and security fixes labels Feb 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.11 only security fixes 3.12 only security fixes 3.13 bugs and security fixes docs Documentation in the Doc dir topic-SSL
Projects
None yet
2 participants