Skip to content

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

@kevinAlbs

Description

@kevinAlbs

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.11only security fixes3.12only security fixes3.13bugs and security fixesdocsDocumentation in the Doc dirtopic-SSL

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions