Skip to content
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

#1275 Add OpenSSL.SSL.Connection.session_reused API. #1276

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Changelog
Versions are year-based with a strict backward-compatibility policy.
The third digit is only for regressions.


- Added ``OpenSSL.SSL.Connection.session_reused()`` to query whether the
current session was reused during the last handshake.
[`#1275 <https://github.com/pyca/pyopenssl/issues/1275>`_]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you want a link to a PR.

I have created the changelong before creating the PR... so at that time, I didn't had a PR ID.



23.3.0 (2023-10-25)
-------------------

Expand Down
19 changes: 19 additions & 0 deletions src/OpenSSL/SSL.py
Original file line number Diff line number Diff line change
Expand Up @@ -2674,6 +2674,25 @@ def set_session(self, session):
result = _lib.SSL_set_session(self._ssl, session._session)
_openssl_assert(result == 1)

def session_reused(self):
"""
Query, whether a reused session was negotiated during the handshake.

During the negotiation, a client can propose to reuse a session.
The server then looks up the session in its cache.
If both client and server agree on the session,
it will be reused and a flag is being set that can be queried by the
application.

Retruns `0` when a new session was negotiated.
Returns `1` when a the session was reused.

:returns: int
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with upstream API, but maybe is best to return a bool


.. versionadded:: NEXT
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure in which version it will be released.

"""
return _lib.SSL_session_reused(self._ssl)

def _get_finished_message(self, function):
"""
Helper to implement :meth:`get_finished` and
Expand Down
121 changes: 112 additions & 9 deletions tests/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ def socket_pair():


def handshake(client, server):
"""
Wait until the TLS handshake is done on both client and server side.
"""
conns = [client, server]
while conns:
for conn in conns:
Expand Down Expand Up @@ -2755,43 +2758,143 @@ def test_set_session_wrong_args(self):
with pytest.raises(TypeError):
connection.set_session(object())

def test_client_set_session(self):
def test_session_reused(self):
"""
`Connection.session_reused`, returns 0 for new connections..
"""
ctx = Context(TLSv1_2_METHOD)
connection = Connection(ctx, None)

assert connection.session_reused() == 0

def test_client_set_session_tls1_2(self):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with both TLS 1.2 and 1.3 tests since the session handling is a big different.

I will refactor the tests to share more code.

"""
`Connection.set_session`, when used prior to a connection being
established, accepts a `Session` instance and causes an attempt to
re-use the session it represents when the SSL handshake is performed.

`Connection.session_reused` is used to query the reuse status.
"""
key = load_privatekey(FILETYPE_PEM, server_key_pem)
cert = load_certificate(FILETYPE_PEM, server_cert_pem)
ctx = Context(TLSv1_2_METHOD)
ctx.use_privatekey(key)
ctx.use_certificate(cert)
ctx.set_session_id(b"unity-test")
server_ctx = Context(TLSv1_2_METHOD)
server_ctx.use_privatekey(key)
server_ctx.use_certificate(cert)
# !!!!
# I have no idea why it works when server-side cache is disabled.
# I guess that this might be because server and client are in the
# same process.
server_ctx.set_session_cache_mode(SSL.SESS_CACHE_OFF)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with explicit context for server and client.

Somehow for this test for TLS 1.2, it works even when cache is off.

For my end to end test, in which I use 2 separate processes, the server cache needs to be enabled for session reuse.

server_ctx.set_session_id(b"unity-test")
server_ctx.set_min_proto_version(TLS1_2_VERSION)
# Session is reused even when client cache is disabled.
client_ctx = Context(TLSv1_2_METHOD)
client_ctx.set_session_cache_mode(SSL.SESS_CACHE_OFF)
client_ctx.set_min_proto_version(TLS1_2_VERSION)
originalSession = None

def makeServer(socket):
server = Connection(ctx, socket)
server = Connection(server_ctx, socket)
server.set_accept_state()
return server

originalServer, originalClient = loopback(server_factory=makeServer)
def makeClient(socket):
client = Connection(client_ctx, socket)
client.set_connect_state()
if originalSession is not None:
client.set_session(originalSession)
return client

originalServer, originalClient = loopback(
server_factory=makeServer, client_factory=makeClient
)
originalSession = originalClient.get_session()

assert originalServer.session_reused() == 0
assert originalClient.session_reused() == 0

resumedServer, resumedClient = loopback(
server_factory=makeServer, client_factory=makeClient
)

# The session on the original connections are not reused.
assert originalServer.session_reused() == 0
assert originalClient.session_reused() == 0

# The sessions on the new connections are reused.
assert resumedServer.session_reused() == 1
assert resumedClient.session_reused() == 1

# This is a proxy: in general, we have no access to any unique
# identifier for the session (new enough versions of OpenSSL expose
# a hash which could be usable, but "new enough" is very, very new).
# Instead, exploit the fact that the master key is re-used if the
# session is re-used. As long as the master key for the two
# connections is the same, the session was re-used!
assert originalServer.master_key() == resumedServer.master_key()
assert originalClient.master_key() == resumedClient.master_key()

def test_client_set_session_tls1_3(self):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea why tls 1.3 fails in this test.

I have it working in a separate manual proof of concept code, in which I have the server with pyOpenSSL and the client is curl.

"""
Test run for `Connection.set_session` and `Connection.session_reused`
when TLS 1.3 is used.
"""
key = load_privatekey(FILETYPE_PEM, server_key_pem)
cert = load_certificate(FILETYPE_PEM, server_cert_pem)
server_ctx = Context(TLS_METHOD)
server_ctx.use_privatekey(key)
server_ctx.use_certificate(cert)

# Session is reused even when server cache is disabled.
server_ctx.set_session_cache_mode(SESS_CACHE_SERVER)
server_ctx.set_session_id(b"unity-test")
server_ctx.set_min_proto_version(TLS1_3_VERSION)
server_ctx.set_options(OP_NO_TICKET)

client_ctx = Context(TLS_METHOD)
client_ctx.set_options(OP_NO_TICKET)
originalSession = None

def makeServer(socket):
server = Connection(server_ctx, socket)
server.set_accept_state()
return server

def makeClient(socket):
client = loopback_client_factory(socket)
client.set_session(originalSession)
client = Connection(client_ctx, socket)
client.set_connect_state()
if originalSession is not None:
client.set_session(originalSession)
return client

originalServer, originalClient = loopback(
server_factory=makeServer, client_factory=makeClient
)
originalSession = originalClient.get_session()

assert originalServer.session_reused() == 0
assert originalClient.session_reused() == 0

resumedServer, resumedClient = loopback(
server_factory=makeServer, client_factory=makeClient
)

# The session on the original connections are not reused.
assert originalServer.session_reused() == 0
assert originalClient.session_reused() == 0

# The sessions on the new connections are reused.
assert resumedServer.session_reused() == 1
assert resumedClient.session_reused() == 1

# This is a proxy: in general, we have no access to any unique
# identifier for the session (new enough versions of OpenSSL expose
# a hash which could be usable, but "new enough" is very, very new).
# Instead, exploit the fact that the master key is re-used if the
# session is re-used. As long as the master key for the two
# connections is the same, the session was re-used!
assert originalServer.master_key() == resumedServer.master_key()
assert originalClient.master_key() == resumedClient.master_key()

def test_set_session_wrong_method(self):
"""
Expand Down