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

gh-63284: Add support for TLS-PSK (pre-shared key) to the ssl module #103181

Merged
merged 22 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9d0f6dc
gh-63284: Add support for TLS-PSK (pre-shared key) to the ssl module
grantramsay Mar 29, 2023
cfddf7b
Fix TLS-PSK for TLS 1.3
grantramsay Apr 3, 2023
603eeef
Merge branch 'main' into fix-issue-63284
arhadthedev May 14, 2023
18302e3
Decode TLS-PSK identities as UTF-8 rather than ASCII
grantramsay Jun 4, 2023
2efc876
Change TLS-PSK version added from 3.12 to 3.13
grantramsay Jun 4, 2023
a56784c
ReSTify NEWS.
gpshead Jun 5, 2023
7a1963d
Merge branch 'main' into fix-issue-63284
gpshead Jul 14, 2023
6f4100c
Set python exceptions raised during C callbacks as unraisable
grantramsay Jul 15, 2023
e4a97ec
Add NULL check for defensive coding
grantramsay Jul 15, 2023
3d75982
Get single value using PyBytes_AsStringAndSize
grantramsay Jul 15, 2023
539ed1f
Do not raise a decode exception if remote side sends invalid UTF-8 du…
grantramsay Jul 16, 2023
4634fc3
Free TLS-PSK callbacks when SSL context is deallocated
grantramsay Jul 16, 2023
d21c322
Use Py_XINCREF and Py_XDECREF for tidier NULL checking
grantramsay Jul 16, 2023
03fac4d
Check return value of SSL_CTX_use_psk_identity_hint
grantramsay Jul 16, 2023
3db3921
Update TLS-PSK documentation
grantramsay Jul 16, 2023
a75f3a7
Regenerate news entry to update the timestamp
grantramsay Jul 16, 2023
851a1e5
Merge branch 'main' into fix-issue-63284
gpshead Nov 26, 2023
4c68974
Update example key strings in the doc.
gpshead Nov 26, 2023
0fb8a3e
Update TLS-PSK documentation
grantramsay Nov 26, 2023
7788589
Regenerate news entry to update the timestamp
grantramsay Nov 26, 2023
11735b9
Code defensively around `z#` vs NULL and 0 length.
gpshead Nov 27, 2023
ad8b249
Merge branch 'fix-issue-63284' of https://github.com/grantramsay/cpyt…
gpshead Nov 27, 2023
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
83 changes: 83 additions & 0 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
general information about TLS, SSL, and certificates, the reader is referred to
the documents in the "See Also" section at the bottom.

This module provides a class, :class:`ssl.SSLSocket`, which is derived from the

Check warning on line 42 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:meth reference target not found: getpeercert

Check warning on line 42 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:meth reference target not found: cipher
:class:`socket.socket` type, and provides a socket-like wrapper that also
encrypts and decrypts the data going over the socket with SSL. It supports
additional methods such as :meth:`getpeercert`, which retrieves the
Expand Down Expand Up @@ -145,7 +145,7 @@
*cadata* is given) or uses :meth:`SSLContext.load_default_certs` to load
default CA certificates.

When :attr:`~SSLContext.keylog_filename` is supported and the environment

Check warning on line 148 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

'envvar' reference target not found: SSLKEYLOGFILE
variable :envvar:`SSLKEYLOGFILE` is set, :func:`create_default_context`
enables key logging.

Expand Down Expand Up @@ -184,7 +184,7 @@

.. versionchanged:: 3.8

Support for key logging to :envvar:`SSLKEYLOGFILE` was added.

Check warning on line 187 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

'envvar' reference target not found: SSLKEYLOGFILE

.. versionchanged:: 3.10

Expand Down Expand Up @@ -311,14 +311,14 @@

.. function:: RAND_status()

Return ``True`` if the SSL pseudo-random number generator has been seeded

Check warning on line 314 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:func reference target not found: ssl.RAND_egd
with 'enough' randomness, and ``False`` otherwise. You can use
:func:`ssl.RAND_egd` and :func:`ssl.RAND_add` to increase the randomness of
the pseudo-random number generator.

.. function:: RAND_add(bytes, entropy)

Mix the given *bytes* into the SSL pseudo-random number generator. The

Check warning on line 321 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:const reference target not found: 0.0
parameter *entropy* (a float) is a lower bound on the entropy contained in
string (so you can always use :const:`0.0`). See :rfc:`1750` for more
information on sources of entropy.
Expand Down Expand Up @@ -401,10 +401,10 @@
:meth:`SSLContext.set_default_verify_paths`. The return value is a
:term:`named tuple` ``DefaultVerifyPaths``:

* :attr:`cafile` - resolved path to cafile or ``None`` if the file doesn't exist,

Check warning on line 404 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:attr reference target not found: cafile
* :attr:`capath` - resolved path to capath or ``None`` if the directory doesn't exist,

Check warning on line 405 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:attr reference target not found: capath
* :attr:`openssl_cafile_env` - OpenSSL's environment key that points to a cafile,

Check warning on line 406 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:attr reference target not found: openssl_cafile_env
* :attr:`openssl_cafile` - hard coded path to a cafile,

Check warning on line 407 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs

py:attr reference target not found: openssl_cafile
* :attr:`openssl_capath_env` - OpenSSL's environment key that points to a capath,
* :attr:`openssl_capath` - hard coded path to a capath directory

Expand Down Expand Up @@ -1987,6 +1987,89 @@
>>> ssl.create_default_context().verify_mode # doctest: +SKIP
<VerifyMode.CERT_REQUIRED: 2>

.. method:: SSLContext.set_psk_client_callback(callback)
grantramsay marked this conversation as resolved.
Show resolved Hide resolved

Enables TLS-PSK (pre-shared key) authentication on a client-side connection.

In general, certificate based authentication should be preferred over this method.

The parameter ``callback`` is a callable object with the signature:
``def callback(hint: str | None) -> tuple[str | None, bytes]``.
grantramsay marked this conversation as resolved.
Show resolved Hide resolved
The ``hint`` parameter is an optional identity hint sent by the server.
The return value is a tuple in the form (client-identity, psk).
Client-identity is an optional string which may be used by the server to
select a corresponding PSK for the client. PSK is a
:term:`bytes-like object` representing the pre-shared key.

Setting ``callback`` to :const:`None` removes any existing callback.

.. note::
When using TLS 1.3:

- the ``hint`` parameter is always :const:`None`.
- client-identity must be a non-empty string.

Example usage::

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
context.maximum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers('PSK')

# A simple lambda:
psk = bytes.fromhex('deadbeef')
context.set_psk_client_callback(lambda hint: (None, psk))

# A table using the hint from the server:
psk_table = { 'ServerId_1': bytes.fromhex('deadbeef'),
'ServerId_2': bytes.fromhex('cafebabe')
}
def callback(hint):
return 'ClientId_1', psk_table[hint]
context.set_psk_client_callback(callback)

.. versionadded:: 3.12
grantramsay marked this conversation as resolved.
Show resolved Hide resolved

.. method:: SSLContext.set_psk_server_callback(callback, identity_hint=None)

Enables TLS-PSK (pre-shared key) authentication on a server-side connection.

In general, certificate based authentication should be preferred over this method.

The parameter ``callback`` is a callable object with the signature:
``def callback(identity: str | None) -> bytes``.
The ``identity`` parameter is an optional identity sent by the client which can
be used to select a corresponding PSK.
The return value is a :term:`bytes-like object` representing the pre-shared key.

Setting ``callback`` to :const:`None` removes any existing callback.

The parameter ``identity_hint`` is an optional identity hint sent to the client.

.. note::
When using TLS 1.3 the ``identity_hint`` parameter is not sent to the client.

Example usage::

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.maximum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers('PSK')

# A simple lambda:
psk = bytes.fromhex('deadbeef')
context.set_psk_server_callback(lambda identity: psk)

# A table using the identity of the client:
psk_table = { 'ClientId_1': bytes.fromhex('deadbeef'),
'ClientId_2': bytes.fromhex('cafed00d')
}
def callback(identity):
return psk_table[identity]
context.set_psk_server_callback(callback, 'ServerId_1')

.. versionadded:: 3.12

.. index:: single: certificates

.. index:: single: X509 certificate
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(call)
STRUCT_FOR_ID(call_exception_handler)
STRUCT_FOR_ID(call_soon)
STRUCT_FOR_ID(callback)
STRUCT_FOR_ID(cancel)
STRUCT_FOR_ID(capath)
STRUCT_FOR_ID(category)
Expand Down Expand Up @@ -445,6 +446,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(hook)
STRUCT_FOR_ID(id)
STRUCT_FOR_ID(ident)
STRUCT_FOR_ID(identity_hint)
STRUCT_FOR_ID(ignore)
STRUCT_FOR_ID(imag)
STRUCT_FOR_ID(importlib)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 88 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4203,6 +4203,94 @@ def test_session_handling(self):
self.assertEqual(str(e.exception),
'Session refers to a different SSLContext.')

@requires_tls_version('TLSv1_2')
def test_psk(self):
psk = bytes.fromhex('deadbeef')

client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
client_context.set_ciphers('PSK')
client_context.set_psk_client_callback(lambda hint: (None, psk))

server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
server_context.set_ciphers('PSK')
server_context.set_psk_server_callback(lambda identity: psk)

# correct PSK should connect
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))

# incorrect PSK should fail
incorrect_psk = bytes.fromhex('cafebabe')
client_context.set_psk_client_callback(lambda hint: (None, incorrect_psk))
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
with self.assertRaises(ssl.SSLError):
s.connect((HOST, server.port))

# identity_hint and client_identity should be sent to the other side
identity_hint = 'identity-hint'
client_identity = 'client-identity'

def client_callback(hint):
self.assertEqual(hint, identity_hint)
return client_identity, psk

def server_callback(identity):
self.assertEqual(identity, client_identity)
return psk

client_context.set_psk_client_callback(client_callback)
server_context.set_psk_server_callback(server_callback, identity_hint)
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))

# adding client callback to server or vice versa raises an exception
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK server callback'):
client_context.set_psk_server_callback(server_callback, identity_hint)
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK client callback'):
server_context.set_psk_client_callback(client_callback)

@requires_tls_version('TLSv1_3')
def test_psk_tls1_3(self):
psk = bytes.fromhex('deadbeef')
identity_hint = 'identity-hint'
client_identity = 'client-identity'

def client_callback(hint):
# identity_hint is not sent to the client in TLS 1.3
self.assertIsNone(hint)
return client_identity, psk

def server_callback(identity):
self.assertEqual(identity, client_identity)
return psk

client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
client_context.minimum_version = ssl.TLSVersion.TLSv1_3
client_context.set_ciphers('PSK')
client_context.set_psk_client_callback(client_callback)

server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
server_context.set_ciphers('PSK')
server_context.set_psk_server_callback(server_callback, identity_hint)

server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))


@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3")
class TestPostHandshakeAuth(unittest.TestCase):
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,7 @@ Ajith Ramachandran
Dhushyanth Ramasamy
Ashwin Ramaswami
Jeff Ramnani
Grant Ramsay
Bayard Randel
Varpu Rantala
Brodie Rao
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for TLS-PSK (pre-shared key) to the ssl module